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