meshcode 2.11.72__tar.gz → 2.11.74__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 (268) hide show
  1. {meshcode-2.11.72 → meshcode-2.11.74}/PKG-INFO +1 -1
  2. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/claude_update.py +6 -1
  4. meshcode-2.11.74/meshcode/hostd.py +576 -0
  5. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/protocol_handler.py +15 -3
  6. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/self_update.py +63 -2
  7. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/SOURCES.txt +2 -0
  9. {meshcode-2.11.72 → meshcode-2.11.74}/pyproject.toml +1 -1
  10. meshcode-2.11.74/tests/test_autonomous_prompt_inject.py +126 -0
  11. {meshcode-2.11.72 → meshcode-2.11.74}/README.md +0 -0
  12. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/__main__.py +0 -0
  13. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/_stop_hook_template.py +0 -0
  14. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/ascii_art.py +0 -0
  15. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/atomic_push.py +0 -0
  16. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/cli.py +0 -0
  17. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/comms_v4.py +0 -0
  18. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/compat.py +0 -0
  19. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/daemon.py +0 -0
  20. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/date_parse.py +0 -0
  21. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/doctor.py +0 -0
  22. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/error_hints.py +0 -0
  23. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/exceptions.py +0 -0
  24. /meshcode-2.11.72/meshcode/hostd.py → /meshcode-2.11.74/meshcode/hostd 3.py +0 -0
  25. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/invites.py +0 -0
  26. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/launcher.py +0 -0
  27. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/launcher_install.py +0 -0
  28. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/__init__.py +0 -0
  29. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/__main__.py +0 -0
  30. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/backend.py +0 -0
  31. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/realtime.py +0 -0
  32. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/server.py +0 -0
  33. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  34. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_backend.py +0 -0
  35. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  36. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  37. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  38. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  39. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  40. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/preferences.py +0 -0
  41. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/protocol_v2.py +0 -0
  42. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/quickstart.py +0 -0
  43. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/rpc_allowlist.py +0 -0
  44. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/run_agent.py +0 -0
  45. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/scripts/check_secrets.py +0 -0
  46. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/scripts/race_rate_harness.py +0 -0
  47. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/secrets.py +0 -0
  48. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/setup_clients.py +0 -0
  49. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/supervisor.py +0 -0
  50. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/up.py +0 -0
  51. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode/upload.py +0 -0
  52. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/comms_v4.py +0 -0
  53. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/__init__.py +0 -0
  54. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
  55. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/cli.py +0 -0
  56. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
  57. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/compat.py +0 -0
  58. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
  59. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
  60. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/invites.py +0 -0
  61. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/launcher.py +0 -0
  62. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
  63. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  64. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  65. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
  66. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  67. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
  68. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  69. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  70. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  71. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/preferences.py +0 -0
  72. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
  73. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
  74. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
  75. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/secrets.py +0 -0
  76. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/self_update.py +0 -0
  77. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
  78. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
  79. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/meshcode/upload.py +0 -0
  80. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/scripts/sentinel.py +0 -0
  81. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_core.py +0 -0
  82. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
  83. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
  84. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
  85. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
  86. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
  87. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
  88. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
  89. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
  90. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
  91. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
  92. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
  93. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
  94. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
  95. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
  96. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
  97. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
  98. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
  99. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
  100. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
  101. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
  102. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
  103. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
  104. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
  105. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
  106. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
  107. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
  108. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
  109. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
  110. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  111. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
  112. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
  113. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
  114. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
  115. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
  116. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
  117. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
  118. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
  119. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
  120. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/comms_v4.py +0 -0
  121. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/__init__.py +0 -0
  122. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
  123. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/cli.py +0 -0
  124. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
  125. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/compat.py +0 -0
  126. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
  127. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
  128. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/invites.py +0 -0
  129. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/launcher.py +0 -0
  130. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
  131. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  132. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  133. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
  134. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  135. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
  136. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  137. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  138. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  139. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/preferences.py +0 -0
  140. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
  141. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
  142. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
  143. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/secrets.py +0 -0
  144. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/self_update.py +0 -0
  145. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
  146. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
  147. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/meshcode/upload.py +0 -0
  148. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/scripts/sentinel.py +0 -0
  149. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_core.py +0 -0
  150. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
  151. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
  152. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
  153. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
  154. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
  155. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
  156. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
  157. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
  158. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
  159. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
  160. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
  161. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/comms_v4.py +0 -0
  162. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
  163. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
  164. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/cli.py +0 -0
  165. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
  166. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/compat.py +0 -0
  167. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
  168. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
  169. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/invites.py +0 -0
  170. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
  171. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
  172. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  173. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  174. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
  175. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  176. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
  177. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  178. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  179. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  180. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
  181. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
  182. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
  183. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
  184. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
  185. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
  186. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
  187. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
  188. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/meshcode/upload.py +0 -0
  189. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
  190. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_core.py +0 -0
  191. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
  192. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
  193. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
  194. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
  195. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
  196. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
  197. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
  198. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
  199. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
  200. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
  201. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
  202. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/PKG-INFO 2 +0 -0
  203. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/SOURCES 2.txt +0 -0
  204. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/dependency_links 2.txt +0 -0
  205. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/dependency_links.txt +0 -0
  206. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/entry_points 2.txt +0 -0
  207. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/entry_points.txt +0 -0
  208. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/requires 2.txt +0 -0
  209. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/requires.txt +0 -0
  210. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/top_level 2.txt +0 -0
  211. {meshcode-2.11.72 → meshcode-2.11.74}/meshcode.egg-info/top_level.txt +0 -0
  212. {meshcode-2.11.72 → meshcode-2.11.74}/setup.cfg +0 -0
  213. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_auto_update_hardening.py +0 -0
  214. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_closegap_1.py +0 -0
  215. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_closegap_2.py +0 -0
  216. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_closegap_3.py +0 -0
  217. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 10.py +0 -0
  218. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 11.py +0 -0
  219. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 12.py +0 -0
  220. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 13.py +0 -0
  221. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 14.py +0 -0
  222. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 15.py +0 -0
  223. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 16.py +0 -0
  224. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 17.py +0 -0
  225. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 18.py +0 -0
  226. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 19.py +0 -0
  227. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 2.py +0 -0
  228. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 20.py +0 -0
  229. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_autonomous_prompt_inject 21.py +0 -0
  230. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 3.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 22.py +0 -0
  231. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 4.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 3.py +0 -0
  232. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 5.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 4.py +0 -0
  233. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 6.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 5.py +0 -0
  234. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 7.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 6.py +0 -0
  235. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 8.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 7.py +0 -0
  236. /meshcode-2.11.72/tests/test_autonomous_prompt_inject 9.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 8.py +0 -0
  237. /meshcode-2.11.72/tests/test_autonomous_prompt_inject.py → /meshcode-2.11.74/tests/test_autonomous_prompt_inject 9.py +0 -0
  238. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_boot_bug_regression.py +0 -0
  239. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_color_truecolor.py +0 -0
  240. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_core.py +0 -0
  241. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_cross_agent_messaging.py +0 -0
  242. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_date_parse.py +0 -0
  243. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_doctor.py +0 -0
  244. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_epistemic_v1_python_sdk.py +0 -0
  245. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  246. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_esc_deaf_state.py +0 -0
  247. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_exceptions.py +0 -0
  248. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_file_upload.py +0 -0
  249. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_init_device_code.py +0 -0
  250. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_install_guard.py +0 -0
  251. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_lease_sigterm_release.py +0 -0
  252. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_mark_read_batch.py +0 -0
  253. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_marketplace_ratings.py +0 -0
  254. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_migration_integrity.py +0 -0
  255. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_realtime_event_freshness.py +0 -0
  256. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_rls_cross_tenant.py +0 -0
  257. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_rpc_grants.py +0 -0
  258. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_rpc_migrations.py +0 -0
  259. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_run_agent_dry_run.py +0 -0
  260. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_run_agent_no_server_import.py +0 -0
  261. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_security_regressions.py +0 -0
  262. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_self_update_user_site.py +0 -0
  263. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_sentinel.py +0 -0
  264. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_setup_path.py +0 -0
  265. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_sleep_signals.py +0 -0
  266. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_status_enum_coverage.py +0 -0
  267. {meshcode-2.11.72 → meshcode-2.11.74}/tests/test_stay_on_loop_hook.py +0 -0
  268. {meshcode-2.11.72 → meshcode-2.11.74}/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.72
3
+ Version: 2.11.74
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.72"
2
+ __version__ = "2.11.74"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -60,9 +60,14 @@ def _is_claude_session() -> bool:
60
60
 
61
61
 
62
62
  def _update_disabled() -> bool:
63
+ # task 399d7b51 FIX A (unify): honor MESHCODE_NO_AUTO_UPDATE too. hostd launcher / run_agent /
64
+ # schtasks set that var to disable ALL launch-time auto-update; previously only the meshcode-pip
65
+ # path checked MESHCODE_NO_UPDATE, so the claude-CLI blocking update still fired (extra hang).
63
66
  if os.environ.get("MESHCODE_NO_UPDATE") == "1":
64
67
  return True
65
- if "--no-update" in sys.argv:
68
+ if os.environ.get("MESHCODE_NO_AUTO_UPDATE") == "1":
69
+ return True
70
+ if "--no-update" in sys.argv or "--no-auto-update" in sys.argv:
66
71
  return True
67
72
  return False
68
73
 
@@ -0,0 +1,576 @@
1
+ """meshcode hostd — host-side daemon: respawn + recycle managed agents.
2
+
3
+ Complements supervisor.py (launchd KeepAlive = process-exit restart only).
4
+ hostd polls the CLOUD for staleness (heartbeat) + recycle triggers that
5
+ launchd cannot see:
6
+
7
+ - RESPAWN: agent's heartbeat is stale (process/daemon-thread dead) and
8
+ desired_state='running' → relaunch. Loop-guard cap 3/10min → 'crashed'.
9
+ - RECYCLE: managed agent exceeds its recycle policy (context% or uptime)
10
+ → relaunch at a task boundary (never mid-task). Recorded separately
11
+ from the crash cap.
12
+
13
+ Contract (mig366/367/368/369, DBA-cleared):
14
+ mc_register_host, mc_agents_needing_respawn, mc_record_respawn,
15
+ mc_record_recycle, mc_set_desired_state, mc_host_recycle_policy.
16
+
17
+ Source of truth is the cloud (mc_agents / mc_host_config), never local
18
+ folders. host_id lives in ~/.meshcode/host_id (generated once).
19
+
20
+ meshcode hostd run — run the daemon loop (foreground)
21
+ meshcode hostd status — show this host's managed agents
22
+ meshcode hostd install — install launchd trigger-start plist
23
+ meshcode hostd uninstall — remove it
24
+
25
+ Design invariants:
26
+ - NEVER respawn desired_state != 'running' (NULL=unmanaged, stopped=
27
+ intentional, crashed=gave-up). The cloud RPC enforces this; we trust it.
28
+ - Single retry / capped respawn — never an infinite relaunch loop.
29
+ - Recycle only at task boundary: we relaunch a recycle candidate only
30
+ when it is NOT actively working (status not in working/online-busy) OR
31
+ the agent has self-flagged 'recycling'. Mid-task agents are skipped.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import os
37
+ import shlex
38
+ import subprocess
39
+ import sys
40
+ import time
41
+ import urllib.request
42
+ import uuid
43
+ from pathlib import Path
44
+ from typing import Optional
45
+
46
+ STATE_DIR = Path.home() / ".meshcode"
47
+ HOST_ID_PATH = STATE_DIR / "host_id"
48
+ LOG_PATH = STATE_DIR / "hostd.log"
49
+
50
+ def _env_poll_interval() -> int:
51
+ # task 399d7b51 SPEED: poll faster so click->desired_state->spawn latency drops (was 45s).
52
+ # Env-configurable (survives upgrade); floor 3s so we never hammer the DB.
53
+ try:
54
+ v = int(os.environ.get("MESHCODE_HOSTD_POLL_SEC", "10"))
55
+ except Exception:
56
+ v = 10
57
+ return max(3, v)
58
+
59
+
60
+ POLL_INTERVAL_SEC = _env_poll_interval() # default 10s (was 45); env MESHCODE_HOSTD_POLL_SEC, floor 3
61
+ STALE_SECONDS = 150 # heartbeat liveness cutoff (QA #3) — do NOT lower (false respawns / boot-storm)
62
+ RESPAWN_TIMEOUT_SEC = 30
63
+ # statuses that mean "actively working" → never recycle mid-task
64
+ BUSY_STATUSES = {"working", "online", "busy"}
65
+
66
+
67
+ def _log(msg: str) -> None:
68
+ line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
69
+ print(line, file=sys.stderr)
70
+ try:
71
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
72
+ with open(LOG_PATH, "a", encoding="utf-8") as f:
73
+ f.write(line + "\n")
74
+ except Exception:
75
+ pass
76
+
77
+
78
+ def get_host_id() -> str:
79
+ """Read ~/.meshcode/host_id, generating a stable UUID on first use."""
80
+ try:
81
+ if HOST_ID_PATH.exists():
82
+ v = HOST_ID_PATH.read_text(encoding="utf-8").strip()
83
+ if v:
84
+ return v
85
+ except Exception:
86
+ pass
87
+ new_id = str(uuid.uuid4())
88
+ try:
89
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
90
+ HOST_ID_PATH.write_text(new_id, encoding="utf-8")
91
+ except Exception:
92
+ pass
93
+ return new_id
94
+
95
+
96
+ # ------------------------------------------------------------------
97
+ # Cloud RPC (PostgREST) — self-contained urllib, no internal client dep
98
+ # ------------------------------------------------------------------
99
+
100
+ def _supabase_cfg() -> tuple:
101
+ # env first (explicit override), then fall back to the SAME resolution the
102
+ # rest of the CLI uses (comms_v4: env -> ~/.meshcode/env -> baked publishable
103
+ # default). hostd runs under launchd with NO env, so the old env-only lookup
104
+ # left it unable to reach the cloud at all ("SUPABASE_URL/KEY not set").
105
+ url = os.environ.get("SUPABASE_URL", "").rstrip("/")
106
+ key = os.environ.get("SUPABASE_KEY") or os.environ.get("MESHCODE_SUPABASE_ANON_KEY") or ""
107
+ if url and key:
108
+ return url, key
109
+ try:
110
+ from meshcode import comms_v4 as _c # type: ignore
111
+ return (url or _c.SUPABASE_URL).rstrip("/"), (key or _c.SUPABASE_KEY)
112
+ except Exception:
113
+ return url, key
114
+
115
+
116
+ def _api_key() -> Optional[str]:
117
+ """The meshcode agent/user api key, used as p_api_key in mc_ RPCs.
118
+
119
+ Resolution mirrors comms_v4._load_api_key_for_cli so hostd authenticates
120
+ the same way `meshcode run` does. CRITICAL: launchd starts hostd with NO
121
+ environment, so the key MUST be resolvable from the keychain (where
122
+ `meshcode login` stores it). The old code only checked env + a
123
+ wrong-shaped profiles.json, so launchd-started hostd always FATAL'd
124
+ ("no api key") and never registered -> dashboard launch had no host.
125
+ """
126
+ # 1) explicit env override
127
+ k = os.environ.get("MESHCODE_API_KEY")
128
+ if k:
129
+ return k
130
+ # 2) keychain — the actual store `meshcode login` writes to. Same source
131
+ # comms_v4._load_api_key_for_cli() uses. This is the launchd fix.
132
+ try:
133
+ from meshcode import secrets as _secrets # type: ignore
134
+ profile = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or _secrets.DEFAULT_PROFILE
135
+ key = _secrets.get_api_key(profile=profile) or ""
136
+ if key:
137
+ return key
138
+ except Exception:
139
+ pass
140
+ # 3) legacy per-profile state file fallback. Handles BOTH the flat
141
+ # {name: {api_key}} and nested {"profiles": {name: {api_key}}} shapes
142
+ # (the flat-only scan silently missed the nested writer).
143
+ try:
144
+ prof = STATE_DIR / "profiles.json"
145
+ if prof.exists():
146
+ data = json.loads(prof.read_text(encoding="utf-8"))
147
+ roots = []
148
+ if isinstance(data, dict):
149
+ roots.append(data)
150
+ if isinstance(data.get("profiles"), dict):
151
+ roots.append(data["profiles"])
152
+ for root in roots:
153
+ for v in root.values():
154
+ if isinstance(v, dict) and v.get("api_key"):
155
+ return v["api_key"]
156
+ except Exception:
157
+ pass
158
+ return None
159
+
160
+
161
+ def _rpc(fn: str, payload: dict) -> Optional[dict]:
162
+ """Call a PostgREST RPC. Returns parsed JSON or None on any failure."""
163
+ url, key = _supabase_cfg()
164
+ if not url or not key:
165
+ _log("WARN: SUPABASE_URL/KEY not set — cannot reach cloud")
166
+ return None
167
+ try:
168
+ req = urllib.request.Request(
169
+ f"{url}/rest/v1/rpc/{fn}",
170
+ data=json.dumps(payload).encode("utf-8"),
171
+ headers={
172
+ "apikey": key,
173
+ "Authorization": f"Bearer {key}",
174
+ "Content-Type": "application/json",
175
+ },
176
+ method="POST",
177
+ )
178
+ with urllib.request.urlopen(req, timeout=15) as resp:
179
+ return json.loads(resp.read().decode("utf-8"))
180
+ except Exception as e:
181
+ _log(f"WARN: rpc {fn} failed: {e}")
182
+ return None
183
+
184
+
185
+ # ------------------------------------------------------------------
186
+ # Respawn
187
+ # ------------------------------------------------------------------
188
+
189
+ def _meshcode_bin() -> str:
190
+ cand = Path(sys.argv[0]).resolve()
191
+ if cand.exists() and cand.name.startswith("meshcode"):
192
+ return str(cand)
193
+ return "meshcode"
194
+
195
+
196
+ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
197
+ """Relaunch `meshcode run <project>/<agent>`.
198
+
199
+ headless=False (default): VISIBLE terminal window. Samuel must SEE it — we
200
+ delegate to protocol_handler._spawn_terminal, the canonical visible spawner
201
+ (macOS: osascript-if-Automation-granted else `open` a .command wrapper, no TCC
202
+ needed; Linux/Windows: native terminals). On failure we WARN loudly and return
203
+ False — we NEVER silently fall back to headless when visible was asked.
204
+
205
+ headless=True (Fleet Control mig404 per-agent flag): background process, NO
206
+ window — for fleet agents that don't need a terminal (like the qa launch).
207
+ """
208
+ target = f"{project}/{agent}"
209
+ bin_ = _meshcode_bin()
210
+ if headless:
211
+ # background, NO terminal — UNIVERSAL macOS/Linux/Windows (task c1a6c6a8, mesh-dev specs).
212
+ # Clean env (a stale CLAUDECODE aborts `meshcode run`); keep crash logs in a per-agent logfile
213
+ # (NOT DEVNULL — we want to debug headless agents that die on boot).
214
+ env = {k: v for k, v in os.environ.items()
215
+ if k not in ("CLAUDECODE", "CLAUDE_CODE_SESSION")}
216
+ env["MESHCODE_NO_AUTO_UPDATE"] = "1"
217
+ env["MESHCODE_NO_UPDATE"] = "1" # belt-and-suspenders: var honored pre-2.11.74 (no launch-hang)
218
+ log_dir = STATE_DIR / "logs"
219
+ try:
220
+ log_dir.mkdir(parents=True, exist_ok=True)
221
+ except Exception:
222
+ pass
223
+ safe = f"{project}__{agent}".replace("/", "_").replace("\\", "_")
224
+ log_path = log_dir / f"{safe}.headless.log"
225
+ logf = None
226
+ try:
227
+ logf = open(log_path, "ab")
228
+ except Exception:
229
+ logf = None
230
+ kwargs = {
231
+ "stdin": subprocess.DEVNULL,
232
+ "stdout": (logf if logf is not None else subprocess.DEVNULL),
233
+ "stderr": subprocess.STDOUT,
234
+ "env": env,
235
+ }
236
+ if sys.platform == "win32":
237
+ # CREATE_NO_WINDOW (0x08000000) | DETACHED_PROCESS (0x08): no console window, fully
238
+ # detached from this daemon. Use the console entry-point (meshcode.exe), NEVER pythonw
239
+ # (it swallows stderr). Ensure venv Scripts + System32 on PATH (Windows PATH cap ~32k).
240
+ kwargs["creationflags"] = 0x08000000 | 0x00000008
241
+ try:
242
+ scripts = str(Path(bin_).resolve().parent)
243
+ except Exception:
244
+ scripts = ""
245
+ sysroot = os.environ.get("SystemRoot", r"C:\Windows")
246
+ base_path = os.pathsep.join(p for p in (scripts, sysroot + r"\System32", sysroot) if p)
247
+ env["PATH"] = (base_path + os.pathsep + env.get("PATH", ""))[:30000]
248
+ else:
249
+ # POSIX: detach into its own session so it survives the daemon + has no controlling tty.
250
+ kwargs["start_new_session"] = True
251
+ try:
252
+ subprocess.Popen([bin_, "run", target], **kwargs)
253
+ _log(f"spawned {target} HEADLESS (no window, {sys.platform}; log={log_path})")
254
+ return True
255
+ except Exception as e:
256
+ _log(f"WARN: headless spawn {target} failed: {e}")
257
+ return False
258
+ finally:
259
+ # child inherited its own fd; safe to drop the daemon's handle.
260
+ if logf is not None:
261
+ try:
262
+ logf.close()
263
+ except Exception:
264
+ pass
265
+ # env hygiene (item1 RC): a stale CLAUDECODE aborts `meshcode run` (exit2). Build the terminal
266
+ # command PER-PLATFORM (mesh-core FIX1, 0x80070002): cmd.exe uses & (NOT ';'), set "V=" to clear
267
+ # (NOT bash unset/export), double-quotes (NOT shlex POSIX), no exec — else Windows Terminal splits
268
+ # on ';' -> file-not-found. Layered ON TOP of the 2.11.72 headless-flags fix (visible branch only).
269
+ if sys.platform == "win32":
270
+ cmd = (f'set "CLAUDECODE=" & set "CLAUDE_CODE_SESSION=" & '
271
+ f'set "MESHCODE_NO_AUTO_UPDATE=1" & set "MESHCODE_NO_UPDATE=1" & '
272
+ f'"{bin_}" run "{target}"')
273
+ else:
274
+ cmd = (f"unset CLAUDECODE CLAUDE_CODE_SESSION; export MESHCODE_NO_AUTO_UPDATE=1; "
275
+ f"export MESHCODE_NO_UPDATE=1; exec {shlex.quote(bin_)} run {shlex.quote(target)}")
276
+ try:
277
+ from meshcode import protocol_handler as _ph
278
+ ok, info = _ph._spawn_terminal(cmd)
279
+ if ok:
280
+ _log(f"spawned {target} in visible terminal ({info})")
281
+ return True
282
+ _log(f"WARN: visible spawn {target} FAILED ({info}) — agent NOT launched. "
283
+ f"macOS: grant Terminal Automation (System Settings > Privacy & Security "
284
+ f"> Automation), or ensure `open` works. NOT falling back to headless.")
285
+ return False
286
+ except Exception as e:
287
+ _log(f"WARN: spawn {target} failed: {e}")
288
+ return False
289
+
290
+
291
+ def _do_respawns(api_key: str, host_id: str) -> int:
292
+ """One respawn sweep. Returns number relaunched."""
293
+ res = _rpc("mc_agents_needing_respawn",
294
+ {"p_api_key": api_key, "p_host_id": host_id, "p_stale_seconds": STALE_SECONDS})
295
+ if not res or not res.get("ok"):
296
+ return 0
297
+ n = 0
298
+ for c in res.get("candidates", []):
299
+ proj, agent = c.get("project_name"), c.get("agent")
300
+ if not proj or not agent:
301
+ continue
302
+ if not c.get("respawn_allowed", True):
303
+ # mig404: not allowed = rate-limited (<60s since last respawn) or at the cap.
304
+ # mc_record_respawn v2 sets desired_state='crashed' ATOMICALLY at the cap, so we
305
+ # do NOT re-record here (that would inflate the count on a mere rate-limit skip).
306
+ _log(f"SKIP respawn {proj}/{agent}: not allowed (count={c.get('respawn_count')}, rate-limited/at-cap)")
307
+ continue
308
+ _log(f"RESPAWN {proj}/{agent} (stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
309
+ if _spawn_agent(proj, agent, headless=bool(c.get("headless"))):
310
+ rec = _rpc("mc_record_respawn",
311
+ {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
312
+ # mig404: give-up is ATOMIC inside mc_record_respawn (sets desired_state='crashed').
313
+ # The old mc_set_desired_state(p_state=crashed) call passed NO agent id = no-op; dropped.
314
+ if rec and rec.get("give_up"):
315
+ _log(f"ALERT {proj}/{agent}: respawn cap hit — marked crashed (atomic). No more respawns.")
316
+ n += 1
317
+ return n
318
+
319
+
320
+ # ------------------------------------------------------------------
321
+ # Recycle — relaunch a managed agent at a TASK BOUNDARY when it exceeds
322
+ # its recycle policy. Uptime-recycle is daemon-driven (needs mig370's
323
+ # project_name in the roster). Context-recycle is agent-cooperative (the
324
+ # agent self-persists handoff + exits at a boundary; our respawn path then
325
+ # relaunches it). Recorded via mc_record_recycle — NEVER counted against
326
+ # the crash respawn cap.
327
+ # ------------------------------------------------------------------
328
+
329
+ _HOSTD_STATE_PATH = STATE_DIR / "hostd_state.json"
330
+
331
+
332
+ def _load_state() -> dict:
333
+ try:
334
+ if _HOSTD_STATE_PATH.exists():
335
+ return json.loads(_HOSTD_STATE_PATH.read_text(encoding="utf-8"))
336
+ except Exception:
337
+ pass
338
+ return {}
339
+
340
+
341
+ def _save_state(st: dict) -> None:
342
+ try:
343
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
344
+ _HOSTD_STATE_PATH.write_text(json.dumps(st), encoding="utf-8")
345
+ except Exception:
346
+ pass
347
+
348
+
349
+ def _do_recycles(api_key: str, host_id: str) -> int:
350
+ """Uptime-based recycle at task boundary. Returns number recycled."""
351
+ cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
352
+ if not cfg or not cfg.get("ok"):
353
+ return 0
354
+ pol = _rpc("mc_host_recycle_policy", {"p_api_key": api_key, "p_host_id": host_id})
355
+ mode = (pol or {}).get("recycle_mode")
356
+ value = (pol or {}).get("recycle_value")
357
+ if mode != "time" or not value:
358
+ return 0 # context-recycle is agent-cooperative; only time is daemon-driven
359
+ st = _load_state()
360
+ now = time.time()
361
+ n = 0
362
+ seen = set()
363
+ for a in cfg.get("agents", []):
364
+ if a.get("desired_state") != "running":
365
+ continue
366
+ proj, agent = a.get("project_name"), a.get("name")
367
+ if not proj or not agent:
368
+ continue
369
+ key = f"{a.get('project_id')}/{agent}"
370
+ seen.add(key)
371
+ first = st.get(key)
372
+ if first is None:
373
+ st[key] = now # start the uptime clock on first observation
374
+ continue
375
+ if (a.get("status") or "") in BUSY_STATUSES:
376
+ continue # task boundary only — never mid-task
377
+ if (now - first) >= float(value) * 3600.0:
378
+ _log(f"RECYCLE {proj}/{agent} (uptime {(now-first)/3600:.1f}h >= {value}h)")
379
+ # Server-authorized clean-exit (task 548c863e, mig 364): SIGNAL the
380
+ # recycle instead of spawning. A direct _spawn_agent here duplicates
381
+ # the still-alive process. mc_request_recycle sets a flag; the
382
+ # agent's wait-loop consumes it, returns must_exit/reason=recycle,
383
+ # exits CLEAN (Stop-hook writes the handoff), then the respawn path
384
+ # (_do_respawns) relaunches it fresh. Recorded via mc_record_recycle
385
+ # so it's NEVER counted against the crash respawn cap.
386
+ req = _rpc("mc_request_recycle",
387
+ {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
388
+ if req and req.get("ok") and req.get("requested"):
389
+ _rpc("mc_record_recycle",
390
+ {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
391
+ st[key] = now # reset clock after signaling recycle
392
+ n += 1
393
+ elif req and req.get("ok") and not req.get("requested"):
394
+ # Agent flipped busy between the roster read and the request —
395
+ # skip; retry next sweep at the next task boundary.
396
+ _log(f"SKIP recycle {proj}/{agent}: {req.get('reason','not_requested')}")
397
+ # prune state for agents no longer managed on this host
398
+ for k in [k for k in st if k not in seen]:
399
+ st.pop(k, None)
400
+ _save_state(st)
401
+ return n
402
+
403
+
404
+ # ------------------------------------------------------------------
405
+ # launchd trigger-start (macOS) — extends supervisor.py pattern.
406
+ # The hostd daemon is NOT autostart-at-login: RunAtLoad=false. It is
407
+ # kicked off on demand (button -> meshcode:// -> `launchctl kickstart`)
408
+ # and KeepAlive keeps it alive while it runs. One job per host.
409
+ # ------------------------------------------------------------------
410
+
411
+ _HOSTD_PLIST_LABEL = "io.meshcode.hostd"
412
+
413
+
414
+ def _hostd_plist_path():
415
+ return Path.home() / "Library" / "LaunchAgents" / f"{_HOSTD_PLIST_LABEL}.plist"
416
+
417
+
418
+ def _hostd_plist_xml() -> str:
419
+ import shutil
420
+ mc = shutil.which("meshcode") or f"{sys.executable} -m meshcode"
421
+ args = (mc.split() + ["hostd", "run"]) if " -m " in mc else [mc, "hostd", "run"]
422
+ args_xml = "\n".join(f" <string>{a}</string>" for a in args)
423
+ logdir = STATE_DIR / "logs"
424
+ logdir.mkdir(parents=True, exist_ok=True)
425
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
426
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
427
+ <plist version="1.0">
428
+ <dict>
429
+ <key>Label</key><string>{_HOSTD_PLIST_LABEL}</string>
430
+ <key>ProgramArguments</key>
431
+ <array>
432
+ {args_xml}
433
+ </array>
434
+ <key>RunAtLoad</key><false/>
435
+ <key>KeepAlive</key><true/>
436
+ <key>ThrottleInterval</key><integer>10</integer>
437
+ <key>StandardOutPath</key><string>{logdir / "hostd.stdout.log"}</string>
438
+ <key>StandardErrorPath</key><string>{logdir / "hostd.stderr.log"}</string>
439
+ <key>EnvironmentVariables</key>
440
+ <dict>
441
+ <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:{os.path.dirname(sys.executable)}</string>
442
+ <key>MESHCODE_NO_AUTO_UPDATE</key><string>1</string>
443
+ </dict>
444
+ </dict>
445
+ </plist>
446
+ """
447
+
448
+
449
+ _HOSTD_TASK_NAME = "MeshCode hostd"
450
+
451
+
452
+ def _hostd_install_windows() -> int:
453
+ """Windows Task Scheduler job (mesh-core ITEM2): at-logon, hidden, restart 3x/1min, no
454
+ time-limit, runs `meshcode hostd run` with auto-update off. Proven model on Samuel's box
455
+ (.meshcode/hostd-launch.cmd + scheduled task 'MeshCode hostd')."""
456
+ mc = shutil.which("meshcode") or "meshcode"
457
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
458
+ (STATE_DIR / "logs").mkdir(parents=True, exist_ok=True)
459
+ launch_cmd = STATE_DIR / "hostd-launch.cmd"
460
+ launch_cmd.write_text(
461
+ "@echo off\r\n"
462
+ 'set "MESHCODE_NO_UPDATE=1"\r\n' # var honored pre-2.11.74 (belt-and-suspenders)
463
+ 'set "MESHCODE_NO_AUTO_UPDATE=1"\r\n' # unified var (honored 2.11.74+)
464
+ 'set "MESHCODE_HOSTD_POLL_SEC=10"\r\n' # faster click->spawn (default 10s, floor 3)
465
+ f'"{mc}" hostd run\r\n',
466
+ encoding="utf-8",
467
+ )
468
+ # Register-ScheduledTask (PowerShell) for fidelity: hidden + restart + unlimited + at-logon.
469
+ ps = (
470
+ '$a = New-ScheduledTaskAction -Execute "cmd.exe" -Argument \'/c "' + str(launch_cmd) + '"\';'
471
+ '$t = New-ScheduledTaskTrigger -AtLogOn;'
472
+ '$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries'
473
+ ' -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3'
474
+ ' -RestartInterval (New-TimeSpan -Minutes 1) -Hidden;'
475
+ '$p = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited;'
476
+ "Register-ScheduledTask -TaskName '" + _HOSTD_TASK_NAME + "' -Action $a -Trigger $t"
477
+ ' -Settings $s -Principal $p -Force | Out-Null'
478
+ )
479
+ r = subprocess.run(["powershell", "-NoProfile", "-Command", ps], capture_output=True, text=True)
480
+ if r.returncode != 0:
481
+ print(f"[hostd] Register-ScheduledTask failed: {r.stderr.strip()}", file=sys.stderr)
482
+ return 1
483
+ print(f"[hostd] installed Windows Task Scheduler job '{_HOSTD_TASK_NAME}' (at-logon, hidden, restart). launcher: {launch_cmd}")
484
+ print(f'[hostd] start now: schtasks /Run /TN "{_HOSTD_TASK_NAME}"')
485
+ return 0
486
+
487
+
488
+ def _hostd_install() -> int:
489
+ import platform
490
+ if platform.system() == "Windows":
491
+ return _hostd_install_windows()
492
+ if platform.system() != "Darwin":
493
+ print("[hostd] launchd install is macOS only (Linux: use systemd --user; or run `meshcode hostd run` under your own supervisor).", file=sys.stderr)
494
+ return 2
495
+ plist = _hostd_plist_path()
496
+ plist.parent.mkdir(parents=True, exist_ok=True)
497
+ plist.write_text(_hostd_plist_xml(), encoding="utf-8")
498
+ subprocess.run(["launchctl", "unload", str(plist)], capture_output=True, text=True)
499
+ r = subprocess.run(["launchctl", "load", str(plist)], capture_output=True, text=True)
500
+ if r.returncode != 0:
501
+ print(f"[hostd] launchctl load failed: {r.stderr.strip()}", file=sys.stderr)
502
+ return 1
503
+ print(f"[hostd] installed (RunAtLoad=false, trigger-start). plist: {plist}")
504
+ print(f"[hostd] start now: launchctl kickstart gui/$(id -u)/{_HOSTD_PLIST_LABEL}")
505
+ return 0
506
+
507
+
508
+ def _hostd_uninstall() -> int:
509
+ if sys.platform == "win32":
510
+ r = subprocess.run(["schtasks", "/Delete", "/TN", _HOSTD_TASK_NAME, "/F"],
511
+ capture_output=True, text=True)
512
+ if r.returncode != 0 and "cannot find" not in (r.stderr + r.stdout).lower():
513
+ print(f"[hostd] schtasks /Delete failed: {r.stderr.strip()}", file=sys.stderr)
514
+ return 1
515
+ print("[hostd] uninstalled (Windows Task Scheduler)")
516
+ return 0
517
+ plist = _hostd_plist_path()
518
+ if not plist.exists():
519
+ print("[hostd] not installed")
520
+ return 0
521
+ subprocess.run(["launchctl", "unload", str(plist)], capture_output=True, text=True)
522
+ plist.unlink(missing_ok=True)
523
+ print("[hostd] uninstalled")
524
+ return 0
525
+
526
+
527
+ def cmd_hostd(args: list) -> int:
528
+ """Entry point for `meshcode hostd ...`."""
529
+ if not args or args[0] in ("-h", "--help"):
530
+ print(__doc__)
531
+ return 0
532
+ sub = args[0]
533
+ host_id = get_host_id()
534
+ api_key = _api_key()
535
+
536
+ if sub == "status":
537
+ if not api_key:
538
+ print("[hostd] no api key configured", file=sys.stderr)
539
+ return 1
540
+ res = _rpc("mc_agents_needing_respawn",
541
+ {"p_api_key": api_key, "p_host_id": host_id, "p_stale_seconds": STALE_SECONDS})
542
+ print(json.dumps({"host_id": host_id, "needing_respawn": res}, indent=2))
543
+ return 0
544
+
545
+ if sub == "install":
546
+ return _hostd_install()
547
+
548
+ if sub == "uninstall":
549
+ return _hostd_uninstall()
550
+
551
+ if sub == "run":
552
+ if not api_key:
553
+ _log("FATAL: no api key — run `meshcode login` (key is read from the keychain)")
554
+ return 1
555
+ # Register this host in mc_host_config so the dashboard can list it as a
556
+ # launch target (Path2 canonical: dashboard -> mc_host_set_agents ->
557
+ # mc_agents.desired_state='running' -> this daemon polls + spawns).
558
+ # mc_host_config_set upserts (host_id, owner_user_id) idempotently.
559
+ _reg = _rpc("mc_host_config_set", {"p_api_key": api_key, "p_host_id": host_id})
560
+ if _reg and _reg.get("ok"):
561
+ _log(f"registered host {host_id} in mc_host_config")
562
+ else:
563
+ _log(f"WARN: host registration failed (dashboard may not list this host): {_reg}")
564
+ _log(f"hostd starting — host_id={host_id} interval={POLL_INTERVAL_SEC}s stale={STALE_SECONDS}s")
565
+ while True:
566
+ try:
567
+ relaunched = _do_respawns(api_key, host_id)
568
+ recycled = _do_recycles(api_key, host_id)
569
+ if relaunched or recycled:
570
+ _log(f"sweep done — {relaunched} respawned, {recycled} recycled")
571
+ except Exception as e:
572
+ _log(f"WARN: sweep error: {e}")
573
+ time.sleep(POLL_INTERVAL_SEC)
574
+
575
+ print(f"[hostd] unknown subcommand: {sub}", file=sys.stderr)
576
+ return 1
@@ -108,10 +108,17 @@ def _spawn_terminal_linux(cmd: str) -> tuple[bool, str]:
108
108
 
109
109
  def _spawn_terminal_windows(cmd: str) -> tuple[bool, str]:
110
110
  """Spawn `cmd` in Windows Terminal (preferred) or cmd.exe."""
111
- if shutil.which("wt.exe"):
111
+ # mesh-core ITEM3: wt.exe is a Store-app shim under %LOCALAPPDATA%\Microsoft\WindowsApps and is
112
+ # frequently NOT on PATH, so shutil.which misses it — resolve that location explicitly.
113
+ wt = shutil.which("wt.exe")
114
+ if not wt:
115
+ cand = Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft" / "WindowsApps" / "wt.exe"
116
+ if cand.exists():
117
+ wt = str(cand)
118
+ if wt:
112
119
  try:
113
120
  # `wt -w 0 nt` opens a new tab in existing window
114
- subprocess.Popen(["wt.exe", "-w", "0", "nt", "cmd", "/k", cmd])
121
+ subprocess.Popen([wt, "-w", "0", "nt", "cmd", "/k", cmd])
115
122
  return True, "wt"
116
123
  except Exception as e:
117
124
  return False, f"wt.exe: {e}"
@@ -155,7 +162,12 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
155
162
  mc_bin = shutil.which("meshcode") or "meshcode"
156
163
 
157
164
  for name in names:
158
- cmd = f"{shlex.quote(mc_bin)} run {shlex.quote(name)}"
165
+ # PER-PLATFORM quoting (mesh-core FIX2): cmd.exe wants double-quotes, not POSIX shlex
166
+ # single-quotes (cmd.exe passes single-quotes through literally -> file-not-found).
167
+ if sys.platform == "win32":
168
+ cmd = f'"{mc_bin}" run "{name}"'
169
+ else:
170
+ cmd = f"{shlex.quote(mc_bin)} run {shlex.quote(name)}"
159
171
  ok, info = _spawn_terminal(cmd)
160
172
  if ok:
161
173
  launched.append(name)