meshcode 2.11.94__tar.gz → 2.11.96__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 (245) hide show
  1. {meshcode-2.11.94 → meshcode-2.11.96}/PKG-INFO +1 -1
  2. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/__init__.py +1 -1
  3. meshcode-2.11.96/meshcode/daemon.py +492 -0
  4. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/hostd.py +35 -3
  5. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/backend.py +319 -31
  6. {meshcode-2.11.94/meshcode-tasks-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/realtime.py +5 -0
  7. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/meshcode_mcp/server.py +48 -0
  8. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/self_update.py +203 -4
  9. meshcode-2.11.96/meshcode/setup_clients.py +1884 -0
  10. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode.egg-info/PKG-INFO +1 -1
  11. meshcode-2.11.96/meshcode.egg-info/SOURCES.txt +89 -0
  12. meshcode-2.11.96/meshcode.egg-info/top_level.txt +1 -0
  13. {meshcode-2.11.94 → meshcode-2.11.96}/pyproject.toml +1 -1
  14. meshcode-2.11.94/meshcode/cli.py +0 -42
  15. meshcode-2.11.94/meshcode/compat.py +0 -174
  16. meshcode-2.11.94/meshcode/daemon.py +0 -0
  17. meshcode-2.11.94/meshcode/error_hints.py +0 -74
  18. meshcode-2.11.94/meshcode/exceptions.py +0 -52
  19. meshcode-2.11.94/meshcode/invites.py +0 -406
  20. meshcode-2.11.94/meshcode/launcher.py +0 -353
  21. meshcode-2.11.94/meshcode/launcher_install.py +0 -414
  22. meshcode-2.11.94/meshcode/meshcode_mcp/__init__.py +0 -22
  23. meshcode-2.11.94/meshcode/meshcode_mcp/__main__.py +0 -62
  24. meshcode-2.11.94/meshcode/meshcode_mcp/backend.py +0 -0
  25. meshcode-2.11.94/meshcode/meshcode_mcp/realtime.py +0 -0
  26. meshcode-2.11.94/meshcode/meshcode_mcp/test_backend.py +0 -86
  27. meshcode-2.11.94/meshcode/meshcode_mcp/test_realtime.py +0 -95
  28. meshcode-2.11.94/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  29. meshcode-2.11.94/meshcode/preferences.py +0 -260
  30. meshcode-2.11.94/meshcode/protocol_v2.py +0 -129
  31. meshcode-2.11.94/meshcode/secrets.py +0 -365
  32. meshcode-2.11.94/meshcode/self_update.py +0 -0
  33. meshcode-2.11.94/meshcode/setup_clients.py +0 -0
  34. meshcode-2.11.94/meshcode/supervisor.py +0 -186
  35. meshcode-2.11.94/meshcode/upload.py +0 -125
  36. meshcode-2.11.94/meshcode-backend-wt/comms_v4.py +0 -0
  37. meshcode-2.11.94/meshcode-backend-wt/meshcode/__init__.py +0 -82
  38. meshcode-2.11.94/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
  39. meshcode-2.11.94/meshcode-backend-wt/meshcode/comms_v4.py +0 -3563
  40. meshcode-2.11.94/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  41. meshcode-2.11.94/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
  42. meshcode-2.11.94/meshcode-backend-wt/meshcode/quickstart.py +0 -148
  43. meshcode-2.11.94/meshcode-backend-wt/meshcode/run_agent.py +0 -0
  44. meshcode-2.11.94/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
  45. meshcode-2.11.94/meshcode-backend-wt/scripts/sentinel.py +0 -257
  46. meshcode-2.11.94/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -387
  47. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -82
  48. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -638
  49. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -42
  50. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
  51. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -174
  52. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -74
  53. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -52
  54. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -406
  55. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -353
  56. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -414
  57. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -22
  58. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -62
  59. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
  60. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
  61. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
  62. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -86
  63. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -95
  64. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  65. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -260
  66. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -129
  67. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -148
  68. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
  69. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -365
  70. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -345
  71. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
  72. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -186
  73. meshcode-2.11.94/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -125
  74. meshcode-2.11.94/meshcode-noun-wt/comms_v4.py +0 -0
  75. meshcode-2.11.94/meshcode-noun-wt/meshcode/__init__.py +0 -82
  76. meshcode-2.11.94/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
  77. meshcode-2.11.94/meshcode-noun-wt/meshcode/cli.py +0 -42
  78. meshcode-2.11.94/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
  79. meshcode-2.11.94/meshcode-noun-wt/meshcode/compat.py +0 -174
  80. meshcode-2.11.94/meshcode-noun-wt/meshcode/error_hints.py +0 -74
  81. meshcode-2.11.94/meshcode-noun-wt/meshcode/exceptions.py +0 -52
  82. meshcode-2.11.94/meshcode-noun-wt/meshcode/invites.py +0 -406
  83. meshcode-2.11.94/meshcode-noun-wt/meshcode/launcher.py +0 -353
  84. meshcode-2.11.94/meshcode-noun-wt/meshcode/launcher_install.py +0 -414
  85. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -22
  86. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -62
  87. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
  88. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  89. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
  90. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
  91. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
  92. meshcode-2.11.94/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  93. meshcode-2.11.94/meshcode-noun-wt/meshcode/preferences.py +0 -260
  94. meshcode-2.11.94/meshcode-noun-wt/meshcode/protocol_v2.py +0 -129
  95. meshcode-2.11.94/meshcode-noun-wt/meshcode/quickstart.py +0 -148
  96. meshcode-2.11.94/meshcode-noun-wt/meshcode/run_agent.py +0 -0
  97. meshcode-2.11.94/meshcode-noun-wt/meshcode/secrets.py +0 -365
  98. meshcode-2.11.94/meshcode-noun-wt/meshcode/self_update.py +0 -345
  99. meshcode-2.11.94/meshcode-noun-wt/meshcode/setup_clients.py +0 -926
  100. meshcode-2.11.94/meshcode-noun-wt/meshcode/supervisor.py +0 -186
  101. meshcode-2.11.94/meshcode-noun-wt/meshcode/upload.py +0 -125
  102. meshcode-2.11.94/meshcode-noun-wt/scripts/sentinel.py +0 -257
  103. meshcode-2.11.94/meshcode-noun-wt/tests/test_core.py +0 -216
  104. meshcode-2.11.94/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -366
  105. meshcode-2.11.94/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -361
  106. meshcode-2.11.94/meshcode-noun-wt/tests/test_exceptions.py +0 -107
  107. meshcode-2.11.94/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -200
  108. meshcode-2.11.94/meshcode-noun-wt/tests/test_migration_integrity.py +0 -176
  109. meshcode-2.11.94/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -236
  110. meshcode-2.11.94/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -255
  111. meshcode-2.11.94/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -387
  112. meshcode-2.11.94/meshcode-noun-wt/tests/test_security_regressions.py +0 -171
  113. meshcode-2.11.94/meshcode-noun-wt/tests/test_sentinel.py +0 -148
  114. meshcode-2.11.94/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -231
  115. meshcode-2.11.94/meshcode-tasks-wt/comms_v4.py +0 -1941
  116. meshcode-2.11.94/meshcode-tasks-wt/meshcode/__init__.py +0 -82
  117. meshcode-2.11.94/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
  118. meshcode-2.11.94/meshcode-tasks-wt/meshcode/cli.py +0 -42
  119. meshcode-2.11.94/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
  120. meshcode-2.11.94/meshcode-tasks-wt/meshcode/compat.py +0 -174
  121. meshcode-2.11.94/meshcode-tasks-wt/meshcode/error_hints.py +0 -74
  122. meshcode-2.11.94/meshcode-tasks-wt/meshcode/exceptions.py +0 -52
  123. meshcode-2.11.94/meshcode-tasks-wt/meshcode/invites.py +0 -406
  124. meshcode-2.11.94/meshcode-tasks-wt/meshcode/launcher.py +0 -353
  125. meshcode-2.11.94/meshcode-tasks-wt/meshcode/launcher_install.py +0 -414
  126. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -22
  127. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -62
  128. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -1261
  129. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
  130. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
  131. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
  132. meshcode-2.11.94/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  133. meshcode-2.11.94/meshcode-tasks-wt/meshcode/preferences.py +0 -260
  134. meshcode-2.11.94/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -129
  135. meshcode-2.11.94/meshcode-tasks-wt/meshcode/quickstart.py +0 -148
  136. meshcode-2.11.94/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
  137. meshcode-2.11.94/meshcode-tasks-wt/meshcode/secrets.py +0 -365
  138. meshcode-2.11.94/meshcode-tasks-wt/meshcode/self_update.py +0 -345
  139. meshcode-2.11.94/meshcode-tasks-wt/meshcode/setup_clients.py +0 -926
  140. meshcode-2.11.94/meshcode-tasks-wt/meshcode/supervisor.py +0 -186
  141. meshcode-2.11.94/meshcode-tasks-wt/meshcode/upload.py +0 -125
  142. meshcode-2.11.94/meshcode-tasks-wt/scripts/sentinel.py +0 -257
  143. meshcode-2.11.94/meshcode-tasks-wt/tests/test_core.py +0 -216
  144. meshcode-2.11.94/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -366
  145. meshcode-2.11.94/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -361
  146. meshcode-2.11.94/meshcode-tasks-wt/tests/test_exceptions.py +0 -107
  147. meshcode-2.11.94/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -200
  148. meshcode-2.11.94/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -176
  149. meshcode-2.11.94/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -236
  150. meshcode-2.11.94/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -255
  151. meshcode-2.11.94/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -387
  152. meshcode-2.11.94/meshcode-tasks-wt/tests/test_security_regressions.py +0 -171
  153. meshcode-2.11.94/meshcode-tasks-wt/tests/test_sentinel.py +0 -148
  154. meshcode-2.11.94/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -231
  155. meshcode-2.11.94/meshcode.egg-info/SOURCES.txt +0 -239
  156. meshcode-2.11.94/meshcode.egg-info/top_level.txt +0 -4
  157. meshcode-2.11.94/tests/test_core.py +0 -216
  158. meshcode-2.11.94/tests/test_cross_agent_messaging.py +0 -366
  159. meshcode-2.11.94/tests/test_esc_deaf_state.py +0 -361
  160. meshcode-2.11.94/tests/test_exceptions.py +0 -107
  161. meshcode-2.11.94/tests/test_mark_read_batch.py +0 -200
  162. meshcode-2.11.94/tests/test_migration_integrity.py +0 -176
  163. meshcode-2.11.94/tests/test_realtime_event_freshness.py +0 -236
  164. meshcode-2.11.94/tests/test_rls_cross_tenant.py +0 -255
  165. meshcode-2.11.94/tests/test_security_regressions.py +0 -171
  166. meshcode-2.11.94/tests/test_sentinel.py +0 -148
  167. meshcode-2.11.94/tests/test_status_enum_coverage.py +0 -231
  168. {meshcode-2.11.94 → meshcode-2.11.96}/README.md +0 -0
  169. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/__main__.py +0 -0
  170. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/_session_handoff_template.py +0 -0
  171. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/_stop_hook_template.py +0 -0
  172. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/ascii_art.py +0 -0
  173. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/atomic_push.py +0 -0
  174. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/claude_update.py +0 -0
  175. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/cli.py +0 -0
  176. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/comms_v4.py +0 -0
  177. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/compat.py +0 -0
  178. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/date_parse.py +0 -0
  179. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/doctor.py +0 -0
  180. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/error_hints.py +0 -0
  181. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/exceptions.py +0 -0
  182. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/invites.py +0 -0
  183. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/launcher.py +0 -0
  184. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/launcher_install.py +0 -0
  185. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/__init__.py +0 -0
  186. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/__main__.py +0 -0
  187. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  188. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/test_backend.py +0 -0
  189. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  190. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  191. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  192. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  193. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  194. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/preferences.py +0 -0
  195. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/protocol_handler.py +0 -0
  196. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/protocol_v2.py +0 -0
  197. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/quickstart.py +0 -0
  198. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/rpc_allowlist.py +0 -0
  199. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/run_agent.py +0 -0
  200. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/scripts/check_secrets.py +0 -0
  201. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/scripts/race_rate_harness.py +0 -0
  202. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/secrets.py +0 -0
  203. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/supervisor.py +0 -0
  204. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode/up.py +0 -0
  205. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/meshcode/upload.py +0 -0
  206. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode.egg-info/dependency_links.txt +0 -0
  207. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode.egg-info/entry_points.txt +0 -0
  208. {meshcode-2.11.94 → meshcode-2.11.96}/meshcode.egg-info/requires.txt +0 -0
  209. {meshcode-2.11.94 → meshcode-2.11.96}/setup.cfg +0 -0
  210. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_auto_update_hardening.py +0 -0
  211. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_autonomous_closegap_1.py +0 -0
  212. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_autonomous_closegap_2.py +0 -0
  213. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_autonomous_closegap_3.py +0 -0
  214. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_autonomous_prompt_inject.py +0 -0
  215. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_boot_bug_regression.py +0 -0
  216. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_color_truecolor.py +0 -0
  217. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_core.py +0 -0
  218. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_cross_agent_messaging.py +0 -0
  219. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_date_parse.py +0 -0
  220. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_doctor.py +0 -0
  221. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_epistemic_v1_python_sdk.py +0 -0
  222. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  223. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_esc_deaf_state.py +0 -0
  224. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_exceptions.py +0 -0
  225. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_file_upload.py +0 -0
  226. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_init_device_code.py +0 -0
  227. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_install_guard.py +0 -0
  228. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_lease_sigterm_release.py +0 -0
  229. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_mark_read_batch.py +0 -0
  230. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_marketplace_ratings.py +0 -0
  231. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_migration_integrity.py +0 -0
  232. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_realtime_event_freshness.py +0 -0
  233. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_rls_cross_tenant.py +0 -0
  234. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_rpc_grants.py +0 -0
  235. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_rpc_migrations.py +0 -0
  236. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_run_agent_dry_run.py +0 -0
  237. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_run_agent_no_server_import.py +0 -0
  238. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_security_regressions.py +0 -0
  239. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_self_update_user_site.py +0 -0
  240. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_sentinel.py +0 -0
  241. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_setup_path.py +0 -0
  242. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_sleep_signals.py +0 -0
  243. {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.96}/tests/test_status_enum_coverage.py +0 -0
  244. {meshcode-2.11.94 → meshcode-2.11.96}/tests/test_stay_on_loop_hook.py +0 -0
  245. {meshcode-2.11.94 → meshcode-2.11.96}/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.94
3
+ Version: 2.11.96
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.94"
2
+ __version__ = "2.11.96"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -0,0 +1,492 @@
1
+ """MeshCode autonomy v1 — headless wake daemon.
2
+
3
+ Per-agent background daemon that polls `mc_agent_should_wake` every ~60s and
4
+ spawns `meshcode run <project> <agent>` only when the backend reports real
5
+ work (urgent task, scheduled action, unread P0 broadcast, kicked status).
6
+
7
+ Layout:
8
+ Darwin: ~/Library/LaunchAgents/io.meshcode.daemon.<project>.<agent>.plist
9
+ Linux: ~/.config/systemd/user/meshcode-daemon-<project>-<agent>.{service,timer}
10
+
11
+ The plist/timer fires `meshcode daemon-tick <project> <agent>` every 60s. The
12
+ tick is short-lived: it asks the RPC, checks anti-flap, and either exits or
13
+ spawns a detached agent process. The agent runs its own session and exits
14
+ when meshcode_wait returns must_exit=True. The daemon stays asleep between
15
+ ticks — zero LLM tokens consumed during idle.
16
+
17
+ Anti-flap:
18
+ - PID file at ~/.meshcode/daemons/<project>.<agent>.json carries last_spawn_at,
19
+ consecutive_failures.
20
+ - 30s hold-down: never re-spawn within 30s of the last spawn.
21
+ - Failure backoff: after 3 consecutive spawn failures inside 5 min, the
22
+ daemon suspends itself for 15 min before retrying.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import platform
29
+ import subprocess
30
+ import sys
31
+ import time
32
+ from pathlib import Path
33
+
34
+ HOME = Path.home()
35
+ DAEMON_STATE_DIR = HOME / ".meshcode" / "daemons"
36
+ CREDS_PATH = HOME / ".meshcode" / "credentials.json"
37
+
38
+ LAUNCHD_DIR = HOME / "Library" / "LaunchAgents"
39
+ SYSTEMD_DIR = HOME / ".config" / "systemd" / "user"
40
+
41
+ POLL_INTERVAL_SECONDS = 60
42
+ HOLD_DOWN_SECONDS = 30
43
+ FAILURE_WINDOW_SECONDS = 300
44
+ FAILURE_BACKOFF_SECONDS = 900
45
+ MAX_FAILURES = 3
46
+
47
+
48
+ def _label(project: str, agent: str) -> str:
49
+ return f"io.meshcode.daemon.{project}.{agent}"
50
+
51
+
52
+ def _systemd_unit_name(project: str, agent: str) -> str:
53
+ return f"meshcode-daemon-{project}-{agent}"
54
+
55
+
56
+ def _plist_path(project: str, agent: str) -> Path:
57
+ return LAUNCHD_DIR / f"{_label(project, agent)}.plist"
58
+
59
+
60
+ def _systemd_service_path(project: str, agent: str) -> Path:
61
+ return SYSTEMD_DIR / f"{_systemd_unit_name(project, agent)}.service"
62
+
63
+
64
+ def _systemd_timer_path(project: str, agent: str) -> Path:
65
+ return SYSTEMD_DIR / f"{_systemd_unit_name(project, agent)}.timer"
66
+
67
+
68
+ def _state_path(project: str, agent: str) -> Path:
69
+ return DAEMON_STATE_DIR / f"{project}.{agent}.json"
70
+
71
+
72
+ def _read_state(project: str, agent: str) -> dict:
73
+ p = _state_path(project, agent)
74
+ if not p.exists():
75
+ return {}
76
+ try:
77
+ return json.loads(p.read_text())
78
+ except Exception:
79
+ return {}
80
+
81
+
82
+ def _write_state(project: str, agent: str, state: dict) -> None:
83
+ DAEMON_STATE_DIR.mkdir(parents=True, exist_ok=True)
84
+ _state_path(project, agent).write_text(json.dumps(state, indent=2))
85
+
86
+
87
+ def _meshcode_bin() -> str:
88
+ """Best-effort: prefer the meshcode console_script on PATH; fall back to
89
+ `python -m meshcode`."""
90
+ from shutil import which
91
+
92
+ found = which("meshcode")
93
+ if found:
94
+ return found
95
+ return f"{sys.executable} -m meshcode"
96
+
97
+
98
+ def _plist_xml(project: str, agent: str) -> str:
99
+ label = _label(project, agent)
100
+ log_dir = HOME / ".meshcode" / "logs"
101
+ log_dir.mkdir(parents=True, exist_ok=True)
102
+ log_out = log_dir / f"daemon.{project}.{agent}.out.log"
103
+ log_err = log_dir / f"daemon.{project}.{agent}.err.log"
104
+ bin_parts = _meshcode_bin().split()
105
+ program_args = "\n ".join(f"<string>{p}</string>" for p in bin_parts) + (
106
+ f"\n <string>daemon-tick</string>\n <string>{project}</string>\n <string>{agent}</string>"
107
+ )
108
+ # Bake current env into the plist so the headless tick can reach the
109
+ # backend without relying on the user's shell rc files.
110
+ sb_url = os.environ.get("SUPABASE_URL", "")
111
+ sb_key = os.environ.get("SUPABASE_KEY") or os.environ.get("SUPABASE_ANON_KEY") or ""
112
+ extra_env = ""
113
+ if sb_url:
114
+ extra_env += f"\n <key>SUPABASE_URL</key><string>{sb_url}</string>"
115
+ if sb_key:
116
+ extra_env += f"\n <key>SUPABASE_KEY</key><string>{sb_key}</string>"
117
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
118
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
119
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
120
+ <plist version="1.0">
121
+ <dict>
122
+ <key>Label</key><string>{label}</string>
123
+ <key>ProgramArguments</key>
124
+ <array>
125
+ {program_args}
126
+ </array>
127
+ <key>StartInterval</key><integer>{POLL_INTERVAL_SECONDS}</integer>
128
+ <key>RunAtLoad</key><true/>
129
+ <key>KeepAlive</key><false/>
130
+ <key>StandardOutPath</key><string>{log_out}</string>
131
+ <key>StandardErrorPath</key><string>{log_err}</string>
132
+ <key>EnvironmentVariables</key>
133
+ <dict>
134
+ <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
135
+ <key>HOME</key><string>{HOME}</string>
136
+ <key>PYTHONUNBUFFERED</key><string>1</string>
137
+ <key>PYTHONIOENCODING</key><string>utf-8</string>{extra_env}
138
+ </dict>
139
+ </dict>
140
+ </plist>
141
+ """
142
+
143
+
144
+ def _systemd_service(project: str, agent: str) -> str:
145
+ bin_cmd = _meshcode_bin()
146
+ sb_url = os.environ.get("SUPABASE_URL", "")
147
+ sb_key = os.environ.get("SUPABASE_KEY") or os.environ.get("SUPABASE_ANON_KEY") or ""
148
+ env_lines = ""
149
+ if sb_url:
150
+ env_lines += f"\nEnvironment=SUPABASE_URL={sb_url}"
151
+ if sb_key:
152
+ env_lines += f"\nEnvironment=SUPABASE_KEY={sb_key}"
153
+ return f"""[Unit]
154
+ Description=MeshCode autonomy daemon ({project}/{agent})
155
+
156
+ [Service]
157
+ Type=oneshot{env_lines}
158
+ ExecStart={bin_cmd} daemon-tick {project} {agent}
159
+ StandardOutput=append:{HOME}/.meshcode/logs/daemon.{project}.{agent}.out.log
160
+ StandardError=append:{HOME}/.meshcode/logs/daemon.{project}.{agent}.err.log
161
+ """
162
+
163
+
164
+ def _systemd_timer(project: str, agent: str) -> str:
165
+ return f"""[Unit]
166
+ Description=MeshCode autonomy daemon timer ({project}/{agent})
167
+
168
+ [Timer]
169
+ OnBootSec=30
170
+ OnUnitActiveSec={POLL_INTERVAL_SECONDS}
171
+ AccuracySec=10
172
+
173
+ [Install]
174
+ WantedBy=default.target
175
+ """
176
+
177
+
178
+ def _run(cmd: list[str]) -> tuple[int, str, str]:
179
+ try:
180
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
181
+ return p.returncode, p.stdout, p.stderr
182
+ except Exception as e:
183
+ return 1, "", str(e)
184
+
185
+
186
+ def cmd_install(project: str, agent: str) -> int:
187
+ if not project or not agent:
188
+ print("[daemon] ERROR: usage: meshcode daemon install <project> <agent>", file=sys.stderr)
189
+ return 2
190
+ DAEMON_STATE_DIR.mkdir(parents=True, exist_ok=True)
191
+ (HOME / ".meshcode" / "logs").mkdir(parents=True, exist_ok=True)
192
+
193
+ sysname = platform.system()
194
+ if sysname == "Darwin":
195
+ LAUNCHD_DIR.mkdir(parents=True, exist_ok=True)
196
+ plist = _plist_path(project, agent)
197
+ plist.write_text(_plist_xml(project, agent))
198
+ print(f"[daemon] wrote {plist}")
199
+ _run(["launchctl", "unload", str(plist)])
200
+ rc, _, err = _run(["launchctl", "load", "-w", str(plist)])
201
+ if rc != 0:
202
+ print(f"[daemon] launchctl load failed: {err}", file=sys.stderr)
203
+ return rc
204
+ print(f"[daemon] launchd registered: {_label(project, agent)} (poll {POLL_INTERVAL_SECONDS}s)")
205
+ return 0
206
+ elif sysname == "Linux":
207
+ SYSTEMD_DIR.mkdir(parents=True, exist_ok=True)
208
+ svc = _systemd_service_path(project, agent)
209
+ tmr = _systemd_timer_path(project, agent)
210
+ svc.write_text(_systemd_service(project, agent))
211
+ tmr.write_text(_systemd_timer(project, agent))
212
+ print(f"[daemon] wrote {svc} + {tmr}")
213
+ _run(["systemctl", "--user", "daemon-reload"])
214
+ rc, _, err = _run(["systemctl", "--user", "enable", "--now", tmr.name])
215
+ if rc != 0:
216
+ print(f"[daemon] systemctl enable failed: {err}", file=sys.stderr)
217
+ return rc
218
+ print(f"[daemon] systemd timer enabled: {tmr.name}")
219
+ return 0
220
+ else:
221
+ print(f"[daemon] ERROR: unsupported platform '{sysname}'. Darwin/Linux only.", file=sys.stderr)
222
+ return 2
223
+
224
+
225
+ def cmd_uninstall(project: str, agent: str) -> int:
226
+ if not project or not agent:
227
+ print("[daemon] ERROR: usage: meshcode daemon uninstall <project> <agent>", file=sys.stderr)
228
+ return 2
229
+ sysname = platform.system()
230
+ if sysname == "Darwin":
231
+ plist = _plist_path(project, agent)
232
+ if plist.exists():
233
+ _run(["launchctl", "unload", str(plist)])
234
+ plist.unlink()
235
+ print(f"[daemon] removed {plist}")
236
+ else:
237
+ print("[daemon] plist not present")
238
+ return 0
239
+ elif sysname == "Linux":
240
+ svc = _systemd_service_path(project, agent)
241
+ tmr = _systemd_timer_path(project, agent)
242
+ _run(["systemctl", "--user", "disable", "--now", tmr.name])
243
+ for p in (svc, tmr):
244
+ if p.exists():
245
+ p.unlink()
246
+ print(f"[daemon] removed {p}")
247
+ _run(["systemctl", "--user", "daemon-reload"])
248
+ return 0
249
+ else:
250
+ print(f"[daemon] ERROR: unsupported platform '{sysname}'.", file=sys.stderr)
251
+ return 2
252
+
253
+
254
+ def cmd_status(project: str, agent: str) -> int:
255
+ if not project or not agent:
256
+ print("[daemon] ERROR: usage: meshcode daemon status <project> <agent>", file=sys.stderr)
257
+ return 2
258
+ state = _read_state(project, agent)
259
+ sysname = platform.system()
260
+ print(f"[daemon] {project}/{agent}")
261
+ if sysname == "Darwin":
262
+ plist = _plist_path(project, agent)
263
+ if plist.exists():
264
+ rc, out, _ = _run(["launchctl", "list", _label(project, agent)])
265
+ print(f" launchd: {'loaded' if rc == 0 else 'not loaded'}")
266
+ else:
267
+ print(" launchd: not installed")
268
+ elif sysname == "Linux":
269
+ tmr = _systemd_timer_path(project, agent)
270
+ if tmr.exists():
271
+ rc, out, _ = _run(["systemctl", "--user", "is-active", tmr.name])
272
+ print(f" systemd timer: {out.strip() or 'unknown'}")
273
+ else:
274
+ print(" systemd timer: not installed")
275
+ if state:
276
+ last_spawn = state.get("last_spawn_at", 0)
277
+ since = time.time() - last_spawn if last_spawn else None
278
+ print(f" last_spawn: {since:.0f}s ago" if since else " last_spawn: never")
279
+ print(f" consecutive_failures: {state.get('consecutive_failures', 0)}")
280
+ if state.get("backoff_until", 0) > time.time():
281
+ print(f" backoff_until: {int(state['backoff_until'] - time.time())}s remaining")
282
+ return 0
283
+
284
+
285
+ def _load_api_key() -> str:
286
+ env_key = os.environ.get("MESHCODE_API_KEY", "")
287
+ if env_key:
288
+ return env_key
289
+ if CREDS_PATH.exists():
290
+ try:
291
+ return json.loads(CREDS_PATH.read_text()).get("api_key", "") or ""
292
+ except Exception:
293
+ return ""
294
+ return ""
295
+
296
+
297
+ def _should_wake(api_key: str, project_id: str, agent: str) -> dict:
298
+ """Call mc_agent_should_wake via Supabase REST. Lightweight — no SDK
299
+ dependencies inside the daemon hot path."""
300
+ import urllib.request
301
+ import urllib.error
302
+
303
+ supabase_url = os.environ.get("SUPABASE_URL") or _supabase_url_from_env()
304
+ supabase_key = os.environ.get("SUPABASE_KEY") or _supabase_key_from_env()
305
+ if not (supabase_url and supabase_key):
306
+ return {"ok": False, "should_wake": False, "error": "no_supabase_env"}
307
+ body = json.dumps({
308
+ "p_api_key": api_key,
309
+ "p_project_id": project_id,
310
+ "p_agent_name": agent,
311
+ }).encode()
312
+ req = urllib.request.Request(
313
+ f"{supabase_url}/rest/v1/rpc/mc_agent_should_wake",
314
+ data=body,
315
+ method="POST",
316
+ headers={
317
+ "Content-Type": "application/json",
318
+ "apikey": supabase_key,
319
+ "Authorization": f"Bearer {supabase_key}",
320
+ },
321
+ )
322
+ try:
323
+ with urllib.request.urlopen(req, timeout=10) as r:
324
+ return json.loads(r.read().decode())
325
+ except urllib.error.HTTPError as e:
326
+ return {"ok": False, "should_wake": False, "http": e.code, "error": e.reason}
327
+ except Exception as e:
328
+ return {"ok": False, "should_wake": False, "error": str(e)}
329
+
330
+
331
+ def _supabase_url_from_env() -> str:
332
+ """No hardcoded fallback — wrong project ref would silently misroute the
333
+ tick. Caller must export SUPABASE_URL or have it baked into the launchd /
334
+ systemd EnvironmentVariables."""
335
+ return os.environ.get("SUPABASE_URL", "")
336
+
337
+
338
+ def _supabase_key_from_env() -> str:
339
+ # Daemon avoids embedding service-role; use the anon publishable key.
340
+ return os.environ.get("SUPABASE_KEY") or os.environ.get("SUPABASE_ANON_KEY") or ""
341
+
342
+
343
+ def _resolve_project_id(api_key: str, project_name: str) -> str:
344
+ """Resolve project name → uuid via mc_resolve_project (read-only, gated by
345
+ api_key). Cached in daemon state to avoid an extra round-trip per tick."""
346
+ state = _read_state(project_name, "_resolver")
347
+ cached = state.get("project_id")
348
+ if cached:
349
+ return cached
350
+ import urllib.request
351
+
352
+ supabase_url = _supabase_url_from_env()
353
+ supabase_key = _supabase_key_from_env()
354
+ if not (supabase_url and supabase_key):
355
+ return ""
356
+ body = json.dumps({"p_api_key": api_key, "p_project_name": project_name}).encode()
357
+ req = urllib.request.Request(
358
+ f"{supabase_url}/rest/v1/rpc/mc_resolve_project",
359
+ data=body,
360
+ method="POST",
361
+ headers={
362
+ "Content-Type": "application/json",
363
+ "apikey": supabase_key,
364
+ "Authorization": f"Bearer {supabase_key}",
365
+ },
366
+ )
367
+ try:
368
+ with urllib.request.urlopen(req, timeout=10) as r:
369
+ payload = json.loads(r.read().decode())
370
+ project_id = payload.get("project_id") or payload.get("id") or ""
371
+ if project_id:
372
+ _write_state(project_name, "_resolver", {"project_id": project_id})
373
+ return project_id
374
+ except Exception:
375
+ return ""
376
+
377
+
378
+ def _spawn_agent(project: str, agent: str) -> bool:
379
+ """Detached `meshcode run <agent>` so the agent owns its own lifetime.
380
+ Daemon does NOT wait — the launchd/systemd tick just kicked off the spawn
381
+ and exits."""
382
+ bin_cmd = _meshcode_bin().split()
383
+ args = bin_cmd + ["run", project, agent]
384
+ log_dir = HOME / ".meshcode" / "logs"
385
+ log_dir.mkdir(parents=True, exist_ok=True)
386
+ out = open(log_dir / f"agent.{project}.{agent}.out.log", "ab", buffering=0)
387
+ err = open(log_dir / f"agent.{project}.{agent}.err.log", "ab", buffering=0)
388
+ try:
389
+ subprocess.Popen(
390
+ args,
391
+ stdout=out,
392
+ stderr=err,
393
+ stdin=subprocess.DEVNULL,
394
+ start_new_session=True,
395
+ close_fds=True,
396
+ )
397
+ return True
398
+ except Exception as e:
399
+ print(f"[daemon] spawn failed: {e}", file=sys.stderr)
400
+ return False
401
+
402
+
403
+ def cmd_tick(project: str, agent: str) -> int:
404
+ """One iteration of the wake loop. Designed to be called by launchd /
405
+ systemd at POLL_INTERVAL_SECONDS cadence."""
406
+ if not project or not agent:
407
+ print("[daemon-tick] ERROR: usage: meshcode daemon-tick <project> <agent>", file=sys.stderr)
408
+ return 2
409
+ now = time.time()
410
+ state = _read_state(project, agent)
411
+
412
+ backoff_until = state.get("backoff_until", 0)
413
+ if backoff_until and now < backoff_until:
414
+ return 0
415
+
416
+ api_key = _load_api_key()
417
+ if not api_key:
418
+ print("[daemon-tick] no api_key (run `meshcode login <key>` first)", file=sys.stderr)
419
+ return 1
420
+
421
+ project_id = _resolve_project_id(api_key, project)
422
+ if not project_id:
423
+ print(f"[daemon-tick] could not resolve project '{project}'", file=sys.stderr)
424
+ return 1
425
+
426
+ resp = _should_wake(api_key, project_id, agent)
427
+ should = bool(resp.get("should_wake"))
428
+ if not should:
429
+ return 0
430
+
431
+ last_spawn = state.get("last_spawn_at", 0)
432
+ if now - last_spawn < HOLD_DOWN_SECONDS:
433
+ return 0
434
+
435
+ spawned = _spawn_agent(project, agent)
436
+ state["last_spawn_at"] = now
437
+ if spawned:
438
+ state["consecutive_failures"] = 0
439
+ state.pop("backoff_until", None)
440
+ state["last_reasons"] = resp.get("reasons", [])
441
+ _write_state(project, agent, state)
442
+ print(f"[daemon-tick] spawned {project}/{agent} reasons={resp.get('reasons')}")
443
+ return 0
444
+
445
+ fails = state.get("consecutive_failures", 0) + 1
446
+ # If the prior burst is older than the failure window, treat this as a
447
+ # fresh first failure so backoff math is monotonic.
448
+ first = state.get("first_failure_at", 0)
449
+ if fails == 1 or (first and now - first > FAILURE_WINDOW_SECONDS):
450
+ state["first_failure_at"] = now
451
+ first = now
452
+ fails = 1
453
+ state["consecutive_failures"] = fails
454
+ state["last_failure_at"] = now
455
+ if fails >= MAX_FAILURES and now - first < FAILURE_WINDOW_SECONDS:
456
+ state["backoff_until"] = now + FAILURE_BACKOFF_SECONDS
457
+ # Clear the burst so the next failure cycle starts fresh.
458
+ state.pop("first_failure_at", None)
459
+ state["consecutive_failures"] = 0
460
+ print(f"[daemon-tick] entering {FAILURE_BACKOFF_SECONDS}s backoff after {fails} failures")
461
+ _write_state(project, agent, state)
462
+ return 1
463
+
464
+
465
+ def main(argv: list[str]) -> int:
466
+ if not argv:
467
+ print(
468
+ "usage:\n"
469
+ " meshcode daemon install <project> <agent>\n"
470
+ " meshcode daemon uninstall <project> <agent>\n"
471
+ " meshcode daemon status <project> <agent>\n"
472
+ " meshcode daemon-tick <project> <agent> # internal, fired by launchd/systemd",
473
+ file=sys.stderr,
474
+ )
475
+ return 2
476
+ sub = argv[0]
477
+ proj = argv[1] if len(argv) > 1 else ""
478
+ name = argv[2] if len(argv) > 2 else ""
479
+ if sub == "install":
480
+ return cmd_install(proj, name)
481
+ if sub == "uninstall":
482
+ return cmd_uninstall(proj, name)
483
+ if sub == "status":
484
+ return cmd_status(proj, name)
485
+ if sub == "tick":
486
+ return cmd_tick(proj, name)
487
+ print(f"[daemon] unknown subcommand: {sub}", file=sys.stderr)
488
+ return 2
489
+
490
+
491
+ if __name__ == "__main__":
492
+ sys.exit(main(sys.argv[1:]))
@@ -446,9 +446,15 @@ def _do_respawns(api_key: str, host_id: str) -> int:
446
446
  # PROMPTLY (the RPC returned it at a 15s stale gate, not STALE_SECONDS) and recorded as a
447
447
  # RECYCLE (mc_record_recycle), NEVER against the crash respawn cap.
448
448
  _is_recycle = bool(c.get("recycle_fast"))
449
+ # 798dd1bd: on-demand recycle with recycle_visible -> reopen a FRESH VISIBLE window
450
+ # (Samuel's "terminals reopen fresh"); else PRESERVE the agent's headless state
451
+ # (don't silently un-headless a headless fleet — backend2 A2). One-shot: mc_record_recycle
452
+ # clears recycle_visible so subsequent respawns use the normal headless flag.
453
+ _visible = _is_recycle and bool(c.get("recycle_visible"))
454
+ _hl = False if _visible else bool(c.get("headless"))
449
455
  _log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
450
- f"(stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
451
- if _spawn_agent(proj, agent, headless=bool(c.get("headless"))):
456
+ f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
457
+ if _spawn_agent(proj, agent, headless=_hl):
452
458
  if _is_recycle:
453
459
  _rpc("mc_record_recycle",
454
460
  {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
@@ -515,13 +521,38 @@ def _record_headless_pid(target: str, pid: int) -> None:
515
521
  try:
516
522
  st = _load_state()
517
523
  pids = st.get("headless_pids") or {}
518
- pids[target] = pid
524
+ pids[target] = pid # overwrite any stale entry for this target (refresh on respawn)
519
525
  st["headless_pids"] = pids
520
526
  _save_state(st)
521
527
  except Exception:
522
528
  pass
523
529
 
524
530
 
531
+ def _gc_headless_pids() -> None:
532
+ """GC dead PIDs from headless_pids (cb90b058) so a stale entry can't mask a live agent +
533
+ the recorded PID stays accurate for enforce. Cheap; runs once per sweep. Best-effort."""
534
+ try:
535
+ st = _load_state()
536
+ pids = st.get("headless_pids") or {}
537
+ if not pids:
538
+ return
539
+ live = {}
540
+ for target, pid in pids.items():
541
+ try:
542
+ os.kill(int(pid), 0) # raises ProcessLookupError if the PID is gone
543
+ live[target] = pid
544
+ except (ProcessLookupError, ValueError):
545
+ pass # dead / bad -> drop
546
+ except Exception:
547
+ live[target] = pid # alive (PermissionError) or unknown -> keep (reuse-guard covers kill-time)
548
+ if len(live) != len(pids):
549
+ st["headless_pids"] = live
550
+ _save_state(st)
551
+ _log(f"GC headless_pids: dropped {len(pids)-len(live)} dead PID(s)")
552
+ except Exception:
553
+ pass
554
+
555
+
525
556
  def _pid_cmdline(pid: int) -> str:
526
557
  """Best-effort command line for a pid (to avoid killing a reused PID). '' on failure."""
527
558
  try:
@@ -1206,6 +1237,7 @@ def cmd_hostd(args: list) -> int:
1206
1237
  recycled = _do_recycles(api_key, host_id)
1207
1238
  ver_recycled = _do_version_recycles(api_key, host_id)
1208
1239
  stopped = _do_stops(api_key, host_id)
1240
+ _gc_headless_pids() # cb90b058: drop dead PIDs (stale entry can't mask a live agent)
1209
1241
  _up = int(time.monotonic() - _spawn_mono)
1210
1242
  if relaunched or recycled or ver_recycled or stopped or enforced:
1211
1243
  _log(f"sweep done (uptime={_up}s) — {relaunched} respawned, {recycled} recycled, {ver_recycled} version-recycled, {stopped} stopped, {enforced} recycle-enforced")