meshcode 2.11.159__tar.gz → 2.11.160__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 (117) hide show
  1. {meshcode-2.11.159 → meshcode-2.11.160}/PKG-INFO +1 -1
  2. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/comms_v4.py +22 -2
  4. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/hostd.py +116 -24
  5. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/self_update.py +35 -0
  6. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.11.159 → meshcode-2.11.160}/pyproject.toml +1 -1
  8. {meshcode-2.11.159 → meshcode-2.11.160}/README.md +0 -0
  9. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/__main__.py +0 -0
  10. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/_launch_smoke.py +0 -0
  11. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/_session_handoff_template.py +0 -0
  12. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/_stop_hook_template.py +0 -0
  13. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/_update_guard.py +0 -0
  14. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/ascii_art.py +0 -0
  15. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/atomic_push.py +0 -0
  16. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/claude_update.py +0 -0
  17. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/cli.py +0 -0
  18. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/compat.py +0 -0
  19. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/daemon.py +0 -0
  20. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/date_parse.py +0 -0
  21. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/doctor.py +0 -0
  22. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/error_hints.py +0 -0
  23. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/exceptions.py +0 -0
  24. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/hooks/__init__.py +0 -0
  25. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/hooks/push_guard.py +0 -0
  26. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/hooks/repo_path_lock.py +0 -0
  27. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/invites.py +0 -0
  28. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/launcher.py +0 -0
  29. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/launcher_install.py +0 -0
  30. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/__init__.py +0 -0
  31. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/__main__.py +0 -0
  32. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/backend.py +0 -0
  33. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/realtime.py +0 -0
  34. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/server.py +0 -0
  35. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  36. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_backend.py +0 -0
  37. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  38. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  39. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  40. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  41. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  42. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/preferences.py +0 -0
  43. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/protocol_handler.py +0 -0
  44. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/protocol_v2.py +0 -0
  45. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/quickstart.py +0 -0
  46. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/rpc_allowlist.py +0 -0
  47. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/run_agent.py +0 -0
  48. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/scripts/check_secrets.py +0 -0
  49. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/scripts/race_rate_harness.py +0 -0
  50. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/secrets.py +0 -0
  51. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/setup_clients.py +0 -0
  52. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/supervisor.py +0 -0
  53. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/up.py +0 -0
  54. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode/upload.py +0 -0
  55. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/SOURCES.txt +0 -0
  56. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/dependency_links.txt +0 -0
  57. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/entry_points.txt +0 -0
  58. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/requires.txt +0 -0
  59. {meshcode-2.11.159 → meshcode-2.11.160}/meshcode.egg-info/top_level.txt +0 -0
  60. {meshcode-2.11.159 → meshcode-2.11.160}/setup.cfg +0 -0
  61. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_auto_update_hardening.py +0 -0
  62. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_autonomous_closegap_1.py +0 -0
  63. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_autonomous_closegap_2.py +0 -0
  64. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_autonomous_closegap_3.py +0 -0
  65. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_autonomous_prompt_inject.py +0 -0
  66. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_boot_bug_regression.py +0 -0
  67. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_color_truecolor.py +0 -0
  68. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_core.py +0 -0
  69. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_cross_agent_messaging.py +0 -0
  70. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_date_parse.py +0 -0
  71. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_doctor.py +0 -0
  72. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_ensure_boot_env_urgent_wake.py +0 -0
  73. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_epistemic_v1_python_sdk.py +0 -0
  74. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  75. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_esc_deaf_state.py +0 -0
  76. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_exceptions.py +0 -0
  77. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_file_upload.py +0 -0
  78. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_fleet_reaper.py +0 -0
  79. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_hostd_launch_pinned_env.py +0 -0
  80. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_hostd_serve_discovery_split.py +0 -0
  81. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_hostd_zombie_sessions.py +0 -0
  82. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_init_device_code.py +0 -0
  83. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_install_guard.py +0 -0
  84. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_launch_smoke.py +0 -0
  85. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_lease_sigterm_release.py +0 -0
  86. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_live_mesh_guard.py +0 -0
  87. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_mark_read_batch.py +0 -0
  88. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_marketplace_ratings.py +0 -0
  89. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_migration_integrity.py +0 -0
  90. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_no_appleevents_on_sweep.py +0 -0
  91. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_preflight_hb_gate.py +0 -0
  92. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_pretrust_claude.py +0 -0
  93. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_push_guard.py +0 -0
  94. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_realtime_event_freshness.py +0 -0
  95. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_replica_base_workspace_fallback.py +0 -0
  96. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_replica_boot_protocol_unconditional.py +0 -0
  97. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_rls_cross_tenant.py +0 -0
  98. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_rm_guard.py +0 -0
  99. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_rpc_grants.py +0 -0
  100. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_rpc_migrations.py +0 -0
  101. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_run_agent_dry_run.py +0 -0
  102. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_run_agent_no_server_import.py +0 -0
  103. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_security_regressions.py +0 -0
  104. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_self_update_user_site.py +0 -0
  105. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_sentinel.py +0 -0
  106. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_session_replay_gate.py +0 -0
  107. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_setup_path.py +0 -0
  108. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_sleep_signals.py +0 -0
  109. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_status_enum_coverage.py +0 -0
  110. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_stay_on_loop_hook.py +0 -0
  111. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_stop_ghost_terminal.py +0 -0
  112. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_task_progress.py +0 -0
  113. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_terminal_lifecycle.py +0 -0
  114. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_up_launch_cmd.py +0 -0
  115. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_update_guard.py +0 -0
  116. {meshcode-2.11.159 → meshcode-2.11.160}/tests/test_urgent_wake_tmux.py +0 -0
  117. {meshcode-2.11.159 → meshcode-2.11.160}/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.159
3
+ Version: 2.11.160
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.159"
2
+ __version__ = "2.11.160"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -2519,6 +2519,7 @@ DIAGNOSTICS:
2519
2519
  doctor [--fix] Diagnose setup issues
2520
2520
  compat Claude Code version compatibility
2521
2521
  upgrade Upgrade meshcode to latest version
2522
+ self-upgrade Unattended deferral-safe upgrade (used by the hostd launcher)
2522
2523
 
2523
2524
  ADMIN:
2524
2525
  clear <proj> <name> Clear inbox
@@ -3103,7 +3104,7 @@ if __name__ == "__main__":
3103
3104
 
3104
3105
  # Auth guard: commands that talk to Supabase need a valid API key.
3105
3106
  # doctor, help, version, login, prefs, launcher don't need auth.
3106
- _NO_AUTH_CMDS = {"doctor", "compat", "upgrade", "help", "--help", "-h", "login",
3107
+ _NO_AUTH_CMDS = {"doctor", "compat", "upgrade", "self-upgrade", "help", "--help", "-h", "login",
3107
3108
  "init", "prefs", "launcher", "--version", "-V", "version",
3108
3109
  "whoami", "profiles", "scan", "setup-path"}
3109
3110
  if cmd not in _NO_AUTH_CMDS:
@@ -4010,6 +4011,25 @@ if __name__ == "__main__":
4010
4011
  print(f" Upgrade cancelled.")
4011
4012
  sys.exit(0)
4012
4013
 
4014
+ elif cmd == "self-upgrade":
4015
+ # meshcode self-upgrade — UNATTENDED, deferral-safe self-upgrade for launcher
4016
+ # pre-steps (task d84cbfa1, version-split self-heal). Distinct from:
4017
+ # - `meshcode upgrade` (INTERACTIVE force escape-hatch; cancels on no-TTY and
4018
+ # overwrites the env even while live agents share it), and
4019
+ # - `meshcode update` (comms STATUS update — unrelated).
4020
+ # Uses the BLOCKING updater with force=False so it DEFERS when a live agent serve
4021
+ # shares the env (never clobbers a running MCP — the spec's live-agent deferral
4022
+ # constraint). Honors MESHCODE_NO_UPDATE / MESHCODE_NO_AUTO_UPDATE (the hostd
4023
+ # launcher clears them for this one explicit step). Never raises; always exits 0
4024
+ # so a failed/offline upgrade can never wedge the launcher.
4025
+ import importlib
4026
+ try:
4027
+ _su = importlib.import_module("meshcode.self_update")
4028
+ _su.check_and_maybe_update_blocking(verbose=True, force=False)
4029
+ except Exception as _e:
4030
+ print(f"[meshcode] self-upgrade skipped: {_e}", file=sys.stderr)
4031
+ sys.exit(0)
4032
+
4013
4033
  elif cmd == "compat":
4014
4034
  from meshcode.compat import check as cc_check, format_report, RECOMMENDED_VERSION
4015
4035
  version, status, entry = cc_check()
@@ -4134,7 +4154,7 @@ if __name__ == "__main__":
4134
4154
  "history", "clear", "unregister", "connect", "disconnect",
4135
4155
  "setup", "add-agent", "run", "go", "invite", "join", "invites", "members",
4136
4156
  "revoke-invite", "revoke-member", "login", "prefs", "launcher",
4137
- "help", "init", "doctor", "compat", "upgrade", "profile", "validate-sessions", "wake-headless",
4157
+ "help", "init", "doctor", "compat", "upgrade", "self-upgrade", "profile", "validate-sessions", "wake-headless",
4138
4158
  "supervisor", "hostd", "up", "upload", "quickstart", "patch-hooks", "wake-all",
4139
4159
  "install-protocol", "launch-url", "launch-batch",
4140
4160
  ]
@@ -97,33 +97,56 @@ def _maybe_self_restart_on_version_drift() -> None:
97
97
  _su.check_and_maybe_update() # non-blocking, detached, TTL-guarded internally
98
98
  except Exception:
99
99
  pass
100
+ # Resolve the best target to converge UP to: the NEWER of
101
+ # (a) the pip-installed site-packages METADATA version, and
102
+ # (b) the newest FINALIZED immutable env under ~/.meshcode/envs/<v>
103
+ # (task d84cbfa1 converge-UP — adopt a version boot-always-latest
104
+ # already built locally even if site-packages is stale).
105
+ # target_py is the interpreter we'd re-exec onto: our own for the metadata
106
+ # path, the env's python for the env-adoption path (so the new code actually
107
+ # loads — the supervisor would relaunch site-packages, not the env).
108
+ target = None
100
109
  try:
101
110
  import importlib.metadata as _ilmd
102
- ondisk = _ilmd.version("meshcode") # reads dist-info METADATA on disk (pip-updated)
111
+ target = _ilmd.version("meshcode") # reads dist-info METADATA on disk (pip-updated)
103
112
  except Exception:
113
+ target = None
114
+ target_py = sys.executable
115
+ via_env = False
116
+ try:
117
+ from meshcode import self_update as _su
118
+ _env = _su.newest_installed_env() # (version, python_path) or None
119
+ if _env:
120
+ _env_ver, _env_py = _env
121
+ if target is None or _su._is_newer(_env_ver, target):
122
+ target, target_py, via_env = _env_ver, str(_env_py), True
123
+ except Exception:
124
+ pass
125
+ if target is None:
104
126
  return
105
127
  try:
106
128
  from meshcode import self_update as _su
107
- newer = _su._is_newer(ondisk, _RUNNING_VERSION)
129
+ newer = _su._is_newer(target, _RUNNING_VERSION)
108
130
  except Exception:
109
131
  def _vt(v):
110
132
  return tuple(int(x) for x in v.split(".") if x.isdigit())
111
133
  try:
112
- newer = _vt(ondisk) > _vt(_RUNNING_VERSION)
134
+ newer = _vt(target) > _vt(_RUNNING_VERSION)
113
135
  except Exception:
114
136
  newer = False
115
137
  if not newer:
116
138
  return
117
- # L3 update-guard (task 2e85c6a8): before re-exec'ing onto the new on-disk version,
118
- # smoke-gate it. enforce -> if the candidate fails launch smoke, do NOT restart
119
- # (stay on known-good), log + alert all commanders. dry_run -> log+alert only,
120
- # restart proceeds. off (default) -> no-op. Fail-open so the guard can't wedge hostd.
139
+ # L3 update-guard (task 2e85c6a8): before re-exec'ing onto the candidate,
140
+ # smoke-gate it (the env python when adopting an env, else our interpreter).
141
+ # enforce -> if the candidate fails launch smoke, do NOT restart (stay on
142
+ # known-good), log + alert all commanders. dry_run -> log+alert only, restart
143
+ # proceeds. off (default) -> no-op. Fail-open so the guard can't wedge hostd.
121
144
  try:
122
145
  from meshcode import _update_guard as _ug
123
- if _ug.guard_blocks_candidate(sys.executable, to_version=ondisk,
146
+ if _ug.guard_blocks_candidate(target_py, to_version=target,
124
147
  from_version=_RUNNING_VERSION,
125
148
  context="hostd_version_drift_restart"):
126
- _log(f"UPDATE-GUARD: on-disk {ondisk} failed launch smoke -> NOT restarting; "
149
+ _log(f"UPDATE-GUARD: candidate {target} failed launch smoke -> NOT restarting; "
127
150
  f"staying on known-good {_RUNNING_VERSION} (MESHCODE_UPDATE_GUARD=enforce).")
128
151
  return
129
152
  except Exception:
@@ -131,42 +154,46 @@ def _maybe_self_restart_on_version_drift() -> None:
131
154
  # Loop-guard (backend2 finding): in a source/dev run, importlib.metadata can report a
132
155
  # pip-installed wheel NEWER than the __init__.py actually executing, so the drift would
133
156
  # PERSIST across restart -> storm. Persist the attempt; if we already tried to reach this
134
- # exact on-disk target recently and didn't advance, skip until the guard window passes.
157
+ # exact target recently and didn't advance, skip until the guard window passes.
135
158
  global _REEXEC_GUARD_LOGGED
136
159
  try:
137
160
  _st = _load_state()
138
161
  except Exception:
139
162
  _st = {}
140
163
  _att = _st.get("reexec_attempt") or {}
141
- if _att.get("target") == ondisk and (time.time() - float(_att.get("at", 0) or 0)) < 600:
164
+ if _att.get("target") == target and (time.time() - float(_att.get("at", 0) or 0)) < 600:
142
165
  if not _REEXEC_GUARD_LOGGED:
143
166
  _REEXEC_GUARD_LOGGED = True
144
- _log(f"version drift {_RUNNING_VERSION}->{ondisk} but a recent restart didn't advance the "
167
+ _log(f"version drift {_RUNNING_VERSION}->{target} but a recent restart didn't advance the "
145
168
  f"running version (source/dev run?). Skipping to avoid a restart storm; retry after 600s.")
146
169
  return
147
- _st["reexec_attempt"] = {"target": ondisk, "at": time.time()}
170
+ _st["reexec_attempt"] = {"target": target, "at": time.time()}
148
171
  try:
149
172
  _save_state(_st)
150
173
  except Exception:
151
174
  pass
152
- _log(f"VERSION DRIFT: running {_RUNNING_VERSION}, on-disk {ondisk} -> restart to load new code "
175
+ _log(f"VERSION DRIFT: running {_RUNNING_VERSION}, target {target}"
176
+ f"{' (env adopt)' if via_env else ' (on-disk)'} -> restart to load new code "
153
177
  f"(Stop kill sweep + daemon fixes). headless_pids persisted.")
154
178
  # Prefer supervisor-restart where one manages hostd: field data (Samuel Mac) showed os.execv
155
179
  # is BLOCKED in the sandboxed runtime and the os._exit->KeepAlive fallback is what actually
156
180
  # restarts. So when a supervisor exists, exit cleanly and let it relaunch on the new wheel.
157
- if _has_supervisor():
181
+ # EXCEPTION: when ADOPTING a versioned env, the supervisor would relaunch site-packages (NOT
182
+ # the env), so skip the clean-exit shortcut and re-exec the env's python directly below.
183
+ if not via_env and _has_supervisor():
158
184
  _log("supervisor present -> clean exit; launchd/systemd/schtasks relaunches on new code")
159
185
  os._exit(0)
160
- # No supervisor (e.g. dev foreground run / sandboxed Mac terminal):
161
- # try execv first (fastest, in-place); if blocked (macOS sandbox), spawn a
162
- # DETACHED hostd on the new wheel and exit. The new process inherits nothing
163
- # from us — clean slate on the on-disk version.
186
+ # No supervisor (e.g. dev foreground run / sandboxed Mac terminal) OR env adoption:
187
+ # try execv first (fastest, in-place; same PID so a supervisor's KeepAlive doesn't
188
+ # double-fire); if blocked (macOS sandbox), spawn a DETACHED hostd on the target and
189
+ # exit. The new process inherits nothing from us — clean slate on the target version;
190
+ # the hostd.lock singleton resolves any race with a supervisor relaunch.
164
191
  try:
165
- os.execv(sys.executable, [sys.executable, "-m", "meshcode"] + sys.argv[1:])
192
+ os.execv(target_py, [target_py, "-m", "meshcode"] + sys.argv[1:])
166
193
  except Exception as e:
167
194
  _log(f"execv blocked ({e}); attempting detached self-relaunch via subprocess")
168
195
  try:
169
- argv = _hostd_run_argv()
196
+ argv = [target_py, "-m", "meshcode", "hostd", "run"] if via_env else _hostd_run_argv()
170
197
  if sys.platform == "win32":
171
198
  # DETACHED_PROCESS: no console inheritance, survives our exit.
172
199
  subprocess.Popen(argv, creationflags=subprocess.DETACHED_PROCESS,
@@ -2580,13 +2607,44 @@ def _hostd_plist_path():
2580
2607
  return Path.home() / "Library" / "LaunchAgents" / f"{_HOSTD_PLIST_LABEL}.plist"
2581
2608
 
2582
2609
 
2610
+ def _hostd_launch_command_path():
2611
+ return STATE_DIR / "hostd-launch.command"
2612
+
2613
+
2583
2614
  def _hostd_plist_xml() -> str:
2584
2615
  import shutil
2585
- mc = shutil.which("meshcode") or f"{sys.executable} -m meshcode"
2586
- args = (mc.split() + ["hostd", "run"]) if " -m " in mc else [mc, "hostd", "run"]
2587
- args_xml = "\n".join(f" <string>{a}</string>" for a in args)
2616
+ mc = shutil.which("meshcode")
2617
+ mc_inv = shlex.quote(mc) if mc else f"{shlex.quote(sys.executable)} -m meshcode"
2588
2618
  logdir = STATE_DIR / "logs"
2589
2619
  logdir.mkdir(parents=True, exist_ok=True)
2620
+ bindir = os.path.dirname(sys.executable)
2621
+ # task d84cbfa1 (version-split self-heal): TODAY ProgramArguments ran `meshcode
2622
+ # hostd run` DIRECTLY. Now generate a /bin/sh wrapper that self-upgrades to the
2623
+ # latest version BEFORE exec'ing hostd, so a machine frozen on an OLD version
2624
+ # autocures on the next launch without depending on the broken in-process upgrade
2625
+ # path (anti chicken-egg). The plist keeps MESHCODE_NO_AUTO_UPDATE=1 in
2626
+ # EnvironmentVariables (in-loop auto-update stays deferral-safe); the wrapper
2627
+ # clears the guards ONLY for the explicit pre-flight upgrade, then restores them.
2628
+ # `self-upgrade` uses the BLOCKING updater with force=False -> it DEFERS when a
2629
+ # live agent serve shares the env, so it never clobbers a running MCP.
2630
+ wrapper = _hostd_launch_command_path()
2631
+ wrapper.write_text(
2632
+ "#!/bin/sh\n"
2633
+ "# MeshCode hostd launcher (task d84cbfa1: version-split self-heal). Generated\n"
2634
+ "# by `meshcode hostd install` — do not edit; it is overwritten on reinstall.\n"
2635
+ f'PATH="{bindir}:$PATH"; export PATH\n'
2636
+ "unset MESHCODE_NO_UPDATE MESHCODE_NO_AUTO_UPDATE\n"
2637
+ f'{mc_inv} self-upgrade >> "{logdir / "hostd-update.log"}" 2>&1 || true\n'
2638
+ "MESHCODE_NO_AUTO_UPDATE=1; export MESHCODE_NO_AUTO_UPDATE\n"
2639
+ f"exec {mc_inv} hostd run\n",
2640
+ encoding="utf-8",
2641
+ )
2642
+ try:
2643
+ os.chmod(wrapper, 0o755)
2644
+ except Exception:
2645
+ pass
2646
+ args = ["/bin/sh", str(wrapper)]
2647
+ args_xml = "\n".join(f" <string>{a}</string>" for a in args)
2590
2648
  return f"""<?xml version="1.0" encoding="UTF-8"?>
2591
2649
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2592
2650
  <plist version="1.0">
@@ -2638,6 +2696,17 @@ def _hostd_install_windows() -> int:
2638
2696
  'set "MESHCODE_NO_UPDATE=1"\r\n' # var honored pre-2.11.74 (belt-and-suspenders)
2639
2697
  'set "MESHCODE_NO_AUTO_UPDATE=1"\r\n' # unified var (honored 2.11.74+)
2640
2698
  'set "MESHCODE_HOSTD_POLL_SEC=10"\r\n' # faster click->spawn (default 10s, floor 3)
2699
+ # task d84cbfa1 (version-split self-heal): self-upgrade to the latest version
2700
+ # BEFORE starting hostd so a machine frozen on an OLD version autocures on the
2701
+ # next launch (anti chicken-egg). Clear the two NO_*UPDATE guards for THIS one
2702
+ # explicit step (they're re-set right after), then run the deferral-safe
2703
+ # `self-upgrade` (force=False -> skips the overwrite if a live agent serve
2704
+ # shares the env). The in-loop auto-update stays guarded by the re-set vars.
2705
+ 'set "MESHCODE_NO_UPDATE="\r\n'
2706
+ 'set "MESHCODE_NO_AUTO_UPDATE="\r\n'
2707
+ f'"{mc}" self-upgrade\r\n'
2708
+ 'set "MESHCODE_NO_UPDATE=1"\r\n'
2709
+ 'set "MESHCODE_NO_AUTO_UPDATE=1"\r\n'
2641
2710
  # F1a (task 48c3f294): launch the SUPERVISOR, not `hostd run` directly.
2642
2711
  # The supervisor owns + revives hostd on CRASH — the recovery the HKCU\Run
2643
2712
  # tier (login-only) lacks (that was the dead launch button on Samuel's box).
@@ -2720,6 +2789,17 @@ def _hostd_install_linux() -> int:
2720
2789
  (STATE_DIR / "logs").mkdir(parents=True, exist_ok=True)
2721
2790
  argv = _hostd_run_argv()
2722
2791
  execstart = " ".join(shlex.quote(a) for a in argv)
2792
+ # task d84cbfa1 (version-split self-heal): self-upgrade BEFORE ExecStart so a host
2793
+ # frozen on an OLD version autocures on the next launch (anti chicken-egg). The unit
2794
+ # sets MESHCODE_NO_*UPDATE=1 globally (keeps the in-loop auto-update deferral-safe),
2795
+ # so the pre-step CLEARS them for this one explicit, deferral-safe `self-upgrade`
2796
+ # (force=False -> skips the overwrite if a live agent serve shares the env). `|| true`
2797
+ # so a failed/offline upgrade never blocks ExecStart.
2798
+ _mc = shutil.which("meshcode")
2799
+ _selfupg = [_mc, "self-upgrade"] if _mc else [sys.executable, "-m", "meshcode", "self-upgrade"]
2800
+ _selfupg_str = " ".join(shlex.quote(a) for a in _selfupg)
2801
+ preexec = "/bin/sh -c " + shlex.quote(
2802
+ f"MESHCODE_NO_UPDATE= MESHCODE_NO_AUTO_UPDATE= {_selfupg_str} || true")
2723
2803
  if shutil.which("systemctl"):
2724
2804
  unit_dir = Path.home() / ".config" / "systemd" / "user"
2725
2805
  unit_dir.mkdir(parents=True, exist_ok=True)
@@ -2728,6 +2808,7 @@ def _hostd_install_linux() -> int:
2728
2808
  "[Service]\nType=simple\n"
2729
2809
  "Environment=MESHCODE_NO_UPDATE=1\nEnvironment=MESHCODE_NO_AUTO_UPDATE=1\n"
2730
2810
  "Environment=MESHCODE_HOSTD_POLL_SEC=10\n"
2811
+ f"ExecStartPre={preexec}\n"
2731
2812
  f"ExecStart={execstart}\nRestart=on-failure\nRestartSec=10\n\n"
2732
2813
  "[Install]\nWantedBy=default.target\n",
2733
2814
  encoding="utf-8",
@@ -3187,6 +3268,17 @@ def cmd_hostd(args: list) -> int:
3187
3268
  f"(budget {_WEDGE_MAX_RESTARTS}/{_WEDGE_WINDOW_S}s)")
3188
3269
  except Exception as e:
3189
3270
  _log(f"WARN: wedge watchdog failed to arm ({e}) — daemon runs unprotected")
3271
+ # task d84cbfa1: converge-UP ONCE at run-startup. A hostd launched on STALE
3272
+ # bytecode (launcher self-upgrade absent on an old machine, or a versioned env
3273
+ # newer than this interpreter) re-execs onto the newest installed version HERE,
3274
+ # BEFORE the poll loop spawns any agent — so the live-agent deferral is never at
3275
+ # risk (no serve children exist yet). No-op when already current; smoke-gated +
3276
+ # loop-guarded inside. Previously this fired only mid-sweep, so a daemon started
3277
+ # stale stayed stale until the first drift sweep.
3278
+ try:
3279
+ _maybe_self_restart_on_version_drift()
3280
+ except Exception as e:
3281
+ _log(f"WARN: startup converge-up check failed ({e}); continuing on {_RUNNING_VERSION}")
3190
3282
  # uptime-since-spawn (core's suggestion): if the daemon dies <2min, the last alive log +
3191
3283
  # the uptime stamped on the FATAL line reveal the <2min pattern for RC.
3192
3284
  _spawn_mono = time.monotonic()
@@ -543,6 +543,41 @@ def _env_python(version: str) -> Path:
543
543
  return ENVS_DIR / version / sub / exe
544
544
 
545
545
 
546
+ def newest_installed_env() -> Optional[tuple]:
547
+ """Return ``(version, python_path)`` for the newest FINALIZED immutable env
548
+ under ``ENVS_DIR`` — one whose ``.build-ok`` marker exists and whose
549
+ python interpreter is present — or None when no built env exists.
550
+
551
+ Used by hostd startup converge-UP (task d84cbfa1): a daemon launched on
552
+ STALE bytecode can adopt the newest version that boot-always-latest already
553
+ built locally, without depending on the in-process upgrade path or a fresh
554
+ PyPI probe (anti chicken-egg). Skips in-flight ``.tmp-*`` builds. Never
555
+ raises."""
556
+ try:
557
+ if not ENVS_DIR.exists():
558
+ return None
559
+ best_ver = None
560
+ best_py = None
561
+ for d in ENVS_DIR.iterdir():
562
+ try:
563
+ if not d.is_dir() or d.name.startswith(".tmp-"):
564
+ continue
565
+ if not (d / _ENV_OK_MARKER).exists():
566
+ continue
567
+ py = _env_python(d.name)
568
+ if not py.exists():
569
+ continue
570
+ if best_ver is None or _is_newer(d.name, best_ver):
571
+ best_ver, best_py = d.name, py
572
+ except Exception:
573
+ continue
574
+ if best_ver is None:
575
+ return None
576
+ return best_ver, best_py
577
+ except Exception:
578
+ return None
579
+
580
+
546
581
  def _prune_stale_tmp_envs(max_age_sec: int = 3600) -> None:
547
582
  """Best-effort removal of leftover .tmp-<ver>-<pid> env dirs left by
548
583
  crashed or locked-rename builds — the Windows version-split litter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.159
3
+ Version: 2.11.160
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.159"
7
+ version = "2.11.160"
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