meshcode 2.10.100__tar.gz → 2.10.101__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 (206) hide show
  1. {meshcode-2.10.100 → meshcode-2.10.101}/PKG-INFO +1 -1
  2. meshcode-2.10.101/meshcode/__init__.py +82 -0
  3. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/server.py +309 -34
  4. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/setup_clients.py +104 -10
  5. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/SOURCES.txt +4 -1
  7. {meshcode-2.10.100 → meshcode-2.10.101}/pyproject.toml +1 -1
  8. meshcode-2.10.101/tests/test_lease_sigterm_release.py +299 -0
  9. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_rpc_migrations.py +65 -0
  10. meshcode-2.10.101/tests/test_stay_on_loop_hook.py +302 -0
  11. meshcode-2.10.101/tests/test_wait_open_tasks_contradiction.py +86 -0
  12. meshcode-2.10.100/meshcode/__init__.py +0 -82
  13. {meshcode-2.10.100 → meshcode-2.10.101}/README.md +0 -0
  14. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/ascii_art.py +0 -0
  15. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/cli.py +0 -0
  16. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/comms_v4.py +0 -0
  17. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/compat.py +0 -0
  18. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/daemon.py +0 -0
  19. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/error_hints.py +0 -0
  20. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/exceptions.py +0 -0
  21. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/invites.py +0 -0
  22. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/launcher.py +0 -0
  23. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/launcher_install.py +0 -0
  24. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/__init__.py +0 -0
  25. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/__main__.py +0 -0
  26. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/backend.py +0 -0
  27. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/realtime.py +0 -0
  28. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_backend.py +0 -0
  29. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  30. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  31. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/preferences.py +0 -0
  32. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/protocol_handler.py +0 -0
  33. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/protocol_v2.py +0 -0
  34. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/quickstart.py +0 -0
  35. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/run_agent.py +0 -0
  36. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/secrets.py +0 -0
  37. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/self_update.py +0 -0
  38. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/supervisor.py +0 -0
  39. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/upload.py +0 -0
  40. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/comms_v4.py +0 -0
  41. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/__init__.py +0 -0
  42. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
  43. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/cli.py +0 -0
  44. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
  45. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/compat.py +0 -0
  46. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
  47. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
  48. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/invites.py +0 -0
  49. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/launcher.py +0 -0
  50. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
  51. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  52. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  53. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
  54. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  55. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
  56. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  57. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  58. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  59. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/preferences.py +0 -0
  60. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
  61. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
  62. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
  63. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/secrets.py +0 -0
  64. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/self_update.py +0 -0
  65. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
  66. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
  67. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/upload.py +0 -0
  68. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/scripts/sentinel.py +0 -0
  69. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_core.py +0 -0
  70. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
  71. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
  72. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
  73. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
  74. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
  75. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
  76. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
  77. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
  78. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
  79. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
  80. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
  81. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
  82. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
  83. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
  84. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
  85. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
  86. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
  87. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
  88. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
  89. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
  90. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
  91. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
  92. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
  93. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
  94. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
  95. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
  96. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
  97. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
  98. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  99. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
  100. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
  101. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
  102. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
  103. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
  104. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
  105. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
  106. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
  107. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
  108. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/comms_v4.py +0 -0
  109. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/__init__.py +0 -0
  110. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
  111. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/cli.py +0 -0
  112. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
  113. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/compat.py +0 -0
  114. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
  115. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
  116. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/invites.py +0 -0
  117. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/launcher.py +0 -0
  118. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
  119. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  120. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  121. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
  122. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  123. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
  124. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  125. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  126. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  127. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/preferences.py +0 -0
  128. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
  129. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
  130. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
  131. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/secrets.py +0 -0
  132. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/self_update.py +0 -0
  133. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
  134. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
  135. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/upload.py +0 -0
  136. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/scripts/sentinel.py +0 -0
  137. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_core.py +0 -0
  138. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
  139. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
  140. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
  141. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
  142. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
  143. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
  144. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
  145. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
  146. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
  147. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
  148. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
  149. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/comms_v4.py +0 -0
  150. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
  151. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
  152. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/cli.py +0 -0
  153. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
  154. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/compat.py +0 -0
  155. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
  156. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
  157. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/invites.py +0 -0
  158. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
  159. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
  160. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  161. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  162. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
  163. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  164. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
  165. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  166. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  167. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  168. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
  169. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
  170. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
  171. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
  172. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
  173. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
  174. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
  175. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
  176. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/upload.py +0 -0
  177. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
  178. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_core.py +0 -0
  179. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
  180. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
  181. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
  182. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
  183. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
  184. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
  185. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
  186. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
  187. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
  188. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
  189. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
  190. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/dependency_links.txt +0 -0
  191. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/entry_points.txt +0 -0
  192. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/requires.txt +0 -0
  193. {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/top_level.txt +0 -0
  194. {meshcode-2.10.100 → meshcode-2.10.101}/setup.cfg +0 -0
  195. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_auto_update_hardening.py +0 -0
  196. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_core.py +0 -0
  197. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_cross_agent_messaging.py +0 -0
  198. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_esc_deaf_state.py +0 -0
  199. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_exceptions.py +0 -0
  200. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_mark_read_batch.py +0 -0
  201. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_migration_integrity.py +0 -0
  202. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_realtime_event_freshness.py +0 -0
  203. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_rls_cross_tenant.py +0 -0
  204. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_security_regressions.py +0 -0
  205. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_sentinel.py +0 -0
  206. {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_status_enum_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.100
3
+ Version: 2.10.101
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -0,0 +1,82 @@
1
+ """MeshCode — Real-time communication between AI agents."""
2
+ __version__ = "2.10.101"
3
+
4
+ # Exception hierarchy — eagerly imported (lightweight, no deps)
5
+ from meshcode.exceptions import ( # noqa: F401
6
+ MeshCodeError,
7
+ AuthError,
8
+ RPCError,
9
+ MeshCodeTimeoutError,
10
+ MeshCodeConnectionError,
11
+ )
12
+
13
+ # Public API — lazy imports to avoid heavy deps at import time
14
+ def __getattr__(name):
15
+ if name == "backend":
16
+ from meshcode.meshcode_mcp import backend
17
+ return backend
18
+ if name in _BACKEND_EXPORTS:
19
+ from meshcode.meshcode_mcp import backend
20
+ return getattr(backend, name)
21
+ if name in _SECRETS_EXPORTS:
22
+ from meshcode import secrets
23
+ return getattr(secrets, name)
24
+ raise AttributeError(f"module 'meshcode' has no attribute {name!r}")
25
+
26
+
27
+ # Backend: core messaging & agent management
28
+ _BACKEND_EXPORTS = {
29
+ "send_message",
30
+ "read_inbox",
31
+ "count_pending",
32
+ "get_board",
33
+ "heartbeat",
34
+ "set_status",
35
+ "register_agent",
36
+ "get_project_id",
37
+ "sb_rpc",
38
+ "task_create",
39
+ "task_list",
40
+ "encrypt_payload",
41
+ "decrypt_payload",
42
+ }
43
+
44
+ # Secrets: credential management
45
+ _SECRETS_EXPORTS = {
46
+ "get_api_key",
47
+ "set_api_key",
48
+ "list_profiles",
49
+ }
50
+
51
+ __all__ = [
52
+ "__version__",
53
+ "backend",
54
+ # Exceptions
55
+ "MeshCodeError",
56
+ "AuthError",
57
+ "RPCError",
58
+ "MeshCodeTimeoutError",
59
+ "MeshCodeConnectionError",
60
+ # Messaging
61
+ "send_message",
62
+ "read_inbox",
63
+ "count_pending",
64
+ # Agent management
65
+ "register_agent",
66
+ "get_project_id",
67
+ "get_board",
68
+ "heartbeat",
69
+ "set_status",
70
+ # Tasks
71
+ "task_create",
72
+ "task_list",
73
+ # Low-level
74
+ "sb_rpc",
75
+ # Encryption
76
+ "encrypt_payload",
77
+ "decrypt_payload",
78
+ # Credentials
79
+ "get_api_key",
80
+ "set_api_key",
81
+ "list_profiles",
82
+ ]
@@ -136,41 +136,110 @@ def _pid_lockfile_path() -> str:
136
136
  return os.path.join(_tempfile.gettempdir(), safe_name)
137
137
 
138
138
 
139
- def _kill_stale_mcp_process() -> None:
140
- """Kill any stale MCP process for this agent found in the lockfile."""
139
+ def _read_pid_lockfile() -> Optional[Dict[str, Any]]:
140
+ """Return {"pid": int, "instance_id": Optional[str]} from the lockfile.
141
+
142
+ Backwards-compatible: pre-2.10.101 lockfiles contain a bare integer
143
+ PID with no instance_id, so callers must tolerate `instance_id=None`.
144
+ """
141
145
  lockfile = _pid_lockfile_path()
142
146
  if not os.path.exists(lockfile):
143
- return
147
+ return None
144
148
  try:
145
149
  with open(lockfile, "r") as f:
146
- old_pid = int(f.read().strip())
147
- if old_pid == os.getpid():
148
- return # It's us
149
- # Check if process is alive
150
- os.kill(old_pid, 0) # Signal 0 = existence check, no actual signal
151
- # Process is alive — kill it gracefully, then forcefully
152
- _mc_log(f"Killing stale MCP process (PID {old_pid})")
153
- os.kill(old_pid, _signal.SIGTERM)
154
- import time
155
- time.sleep(1)
150
+ raw = f.read().strip()
151
+ if not raw:
152
+ return None
156
153
  try:
157
- os.kill(old_pid, 0) # Still alive?
158
- os.kill(old_pid, _signal.SIGKILL)
159
- _mc_log(f"Force-killed stale MCP process (PID {old_pid})")
160
- except OSError:
161
- pass # Already dead after SIGTERM
162
- except (ValueError, FileNotFoundError):
163
- pass # Corrupt or missing lockfile
154
+ data = json.loads(raw)
155
+ if isinstance(data, dict) and "pid" in data:
156
+ return {"pid": int(data["pid"]),
157
+ "instance_id": data.get("instance_id")}
158
+ except (ValueError, TypeError):
159
+ pass
160
+ return {"pid": int(raw), "instance_id": None}
161
+ except (ValueError, FileNotFoundError, OSError):
162
+ return None
163
+
164
+
165
+ def _kill_stale_mcp_process() -> None:
166
+ """Kill any stale MCP process for this agent and release its DB lease.
167
+
168
+ The dying process ignores SIGTERM (see run_server() _diag_ignore — it
169
+ keeps MCP alive during tool cancellation) and so always falls through
170
+ to SIGKILL. SIGKILL skips the lifespan shutdown handler that calls
171
+ `mc_release_agent_lease`, leaving an orphan lease that blocks the
172
+ new instance's acquire path for 6-8s of retries (2s + 4s + force
173
+ release). Claude Code surfaces that as "MCP server failed at launch".
174
+
175
+ Fix: read the dying process's instance_id from the lockfile (new
176
+ format) and explicitly release its lease right after SIGKILL, so
177
+ the new instance hits a clean slate on its first acquire attempt.
178
+ """
179
+ info = _read_pid_lockfile()
180
+ if not info:
181
+ return
182
+ old_pid = info.get("pid")
183
+ old_instance_id = info.get("instance_id")
184
+ if old_pid is None or old_pid == os.getpid():
185
+ return
186
+ try:
187
+ os.kill(old_pid, 0) # existence check
164
188
  except OSError:
165
- pass # Process not found (already dead)
189
+ # Process already dead; lockfile orphaned. Still release lease
190
+ # if we know its instance_id so a half-cleaned crash doesn't
191
+ # block the new acquire.
192
+ if old_instance_id:
193
+ _release_lease_for_instance(old_instance_id, reason="lockfile-orphan")
194
+ return
195
+ _mc_log(f"Killing stale MCP process (PID {old_pid})")
196
+ try:
197
+ os.kill(old_pid, _signal.SIGTERM)
198
+ except OSError:
199
+ pass
200
+ import time
201
+ time.sleep(1)
202
+ try:
203
+ os.kill(old_pid, 0)
204
+ os.kill(old_pid, _signal.SIGKILL)
205
+ _mc_log(f"Force-killed stale MCP process (PID {old_pid})")
206
+ except OSError:
207
+ pass # already dead after SIGTERM (rare — handler ignores it)
208
+ if old_instance_id:
209
+ _release_lease_for_instance(old_instance_id, reason="post-sigkill")
210
+
211
+
212
+ def _release_lease_for_instance(instance_id: str, reason: str = "") -> None:
213
+ """Best-effort release of the dying process's DB lease.
214
+
215
+ Called right after SIGKILL so the new instance's `_acquire_lease`
216
+ path doesn't have to burn 6-8s on the retry+force-clear fallback.
217
+ Failures are non-fatal — the existing retry path remains the safety
218
+ net.
219
+ """
220
+ try:
221
+ be.sb_rpc("mc_release_agent_lease", {
222
+ "p_api_key": _get_api_key(),
223
+ "p_project_id": _PROJECT_ID,
224
+ "p_agent_name": AGENT_NAME,
225
+ "p_instance_id": instance_id,
226
+ })
227
+ _mc_log(f"released stale lease (instance={instance_id}, {reason})")
228
+ except Exception as e:
229
+ _mc_log(f"could not release stale lease ({reason}): {e}", "warn")
166
230
 
167
231
 
168
232
  def _write_pid_lockfile() -> None:
169
- """Write current PID to lockfile."""
233
+ """Write current PID + instance_id to lockfile (JSON, backwards-compat).
234
+
235
+ The instance_id lets the next `_kill_stale_mcp_process` call release
236
+ the dying lease, eliminating the 6-8s retry hang on relaunch.
237
+ """
170
238
  lockfile = _pid_lockfile_path()
171
239
  try:
240
+ payload = {"pid": os.getpid(), "instance_id": _INSTANCE_ID}
172
241
  with open(lockfile, "w") as f:
173
- f.write(str(os.getpid()))
242
+ f.write(json.dumps(payload))
174
243
  except Exception as e:
175
244
  _mc_log(f"Warning: couldn't write PID lockfile: {e}", level="warn")
176
245
 
@@ -995,6 +1064,7 @@ def _acquire_lease() -> bool:
995
1064
  _mc_log(f" lease failed after 3 attempts — proceeding anyway", "warn")
996
1065
  return True
997
1066
 
1067
+ _kill_stale_mcp_process()
998
1068
  if not _acquire_lease():
999
1069
  sys.exit(2)
1000
1070
 
@@ -1110,6 +1180,106 @@ def _release_lease() -> None:
1110
1180
  pass
1111
1181
 
1112
1182
 
1183
+ # ── Time-boxed lease release for shutdown paths ────────────────
1184
+ #
1185
+ # RCA #2 (project_mesh_commander_mcp_failed_at_launch, 2026-05-06):
1186
+ # SIGKILL leaves the DB lease orphaned, causing a 6-8s retry hang on
1187
+ # relaunch. Mig 2.10.101 added lockfile-based release on the NEW
1188
+ # process's boot (_kill_stale_mcp_process); this helper closes the
1189
+ # DYING process's gap by making the lifespan-finally release
1190
+ # synchronous + time-boxed instead of a daemon thread that may die
1191
+ # with the process before its HTTP call completes.
1192
+ #
1193
+ # Idempotent: only runs once per process via _SHUTDOWN_LEASE_RELEASED.
1194
+ # Returns True if release thread finished within timeout (success or
1195
+ # RPC error), False if it timed out — fallback in that case is the
1196
+ # lockfile-based release on next boot.
1197
+ _SHUTDOWN_LEASE_RELEASED = False
1198
+
1199
+
1200
+ def _release_lease_synchronous(timeout_s: float = 1.5) -> bool:
1201
+ global _SHUTDOWN_LEASE_RELEASED
1202
+ if _SHUTDOWN_LEASE_RELEASED:
1203
+ return True
1204
+ _SHUTDOWN_LEASE_RELEASED = True
1205
+ done = _threading.Event()
1206
+
1207
+ def _do():
1208
+ try:
1209
+ _release_lease()
1210
+ except Exception:
1211
+ pass
1212
+ done.set()
1213
+
1214
+ try:
1215
+ _threading.Thread(
1216
+ target=_do, daemon=True, name="meshcode-shutdown-lease-release"
1217
+ ).start()
1218
+ except Exception:
1219
+ # Threading machinery already torn down — fall back to direct call.
1220
+ try:
1221
+ _release_lease()
1222
+ except Exception:
1223
+ pass
1224
+ return True
1225
+ return done.wait(timeout=timeout_s)
1226
+
1227
+
1228
+ def _shutdown_signal_handler(signum, frame): # pragma: no cover - signal handler
1229
+ """Graceful-shutdown SIGTERM/SIGINT handler (PROTO-LEASE-SIGTERM-RELEASE).
1230
+
1231
+ Default OFF: run_server installs SIG_IGN to keep MCP alive during
1232
+ Claude Code ESC cancellations (see _diag_ignore). Opt-in via:
1233
+ MESHCODE_GRACEFUL_SIGTERM=1 # SIGTERM only (recommended)
1234
+ MESHCODE_GRACEFUL_SIGINT=1 # SIGINT too (advanced; conflicts
1235
+ # with ESC-cancellation safety)
1236
+ when running in a process supervisor that uses SIGTERM as the
1237
+ canonical shutdown signal (systemd, docker stop, k8s pod terminate).
1238
+ """
1239
+ try:
1240
+ sys.stderr.write(
1241
+ f"[meshcode-mcp] shutdown signal {signum} — releasing lease "
1242
+ f"(instance={_INSTANCE_ID})\n"
1243
+ )
1244
+ sys.stderr.flush()
1245
+ except Exception:
1246
+ pass
1247
+ if os.environ.get("MESHCODE_UPDATING") == "1":
1248
+ # Auto-update re-exec is in flight; the new process will release
1249
+ # the old lease via _kill_stale_mcp_process / lockfile. Don't
1250
+ # race the old image into a redundant release.
1251
+ os._exit(0)
1252
+ try:
1253
+ _release_lease_synchronous(timeout_s=1.5)
1254
+ except Exception:
1255
+ pass
1256
+ os._exit(0)
1257
+
1258
+
1259
+ def _install_shutdown_signal_handlers() -> None:
1260
+ """Register SIGTERM (+ SIGINT) graceful-shutdown handlers if opt-in.
1261
+
1262
+ Called from the lifespan startup after agent identity is loaded. Skips
1263
+ silently when the env flag is not set so default Claude Code ESC
1264
+ behavior (SIG_IGN per run_server) is preserved.
1265
+ """
1266
+ flag = os.environ.get("MESHCODE_GRACEFUL_SIGTERM", "").lower()
1267
+ if flag not in ("1", "true", "yes"):
1268
+ return
1269
+ import signal as _sig_mod
1270
+ try:
1271
+ _sig_mod.signal(_sig_mod.SIGTERM, _shutdown_signal_handler)
1272
+ _mc_log("registered SIGTERM graceful-shutdown handler (MESHCODE_GRACEFUL_SIGTERM=1)")
1273
+ except (ValueError, OSError) as e:
1274
+ _mc_log(f"could not register SIGTERM handler: {e}", "warn")
1275
+ if os.environ.get("MESHCODE_GRACEFUL_SIGINT", "").lower() in ("1", "true", "yes"):
1276
+ try:
1277
+ _sig_mod.signal(_sig_mod.SIGINT, _shutdown_signal_handler)
1278
+ _mc_log("registered SIGINT graceful-shutdown handler (MESHCODE_GRACEFUL_SIGINT=1)")
1279
+ except (ValueError, OSError) as e:
1280
+ _mc_log(f"could not register SIGINT handler: {e}", "warn")
1281
+
1282
+
1113
1283
  # ── Crash logging + graceful shutdown ──────────────────────────
1114
1284
  _SHUTDOWN_LOGGED = False
1115
1285
 
@@ -1142,11 +1312,21 @@ def _log_crash_to_db(reason: str = "unknown", error_detail: str = "") -> None:
1142
1312
 
1143
1313
 
1144
1314
 
1145
- # NOTE: Do NOT install custom SIGTERM handlers or atexit hooks here — network
1146
- # calls inside them can deadlock the event loop. The lease is released by the
1147
- # lifespan shutdown handler instead.
1148
- #
1149
- # SIGINT handling is special see run_server() for why we set SIG_IGN there.
1315
+ # NOTE on signal handling:
1316
+ # - run_server() installs SIG_IGN for SIGINT/SIGTERM/SIGHUP/SIGPIPE so
1317
+ # Claude Code ESC cancellation cannot kill the MCP subprocess
1318
+ # (cancellation safety).
1319
+ # - PROTO-LEASE-SIGTERM-RELEASE adds an OPT-IN SIGTERM (and optional
1320
+ # SIGINT) graceful-shutdown handler in _shutdown_signal_handler /
1321
+ # _install_shutdown_signal_handlers above. Default off; turn on via
1322
+ # MESHCODE_GRACEFUL_SIGTERM=1 in process supervisors that use SIGTERM
1323
+ # as canonical shutdown signal (systemd / docker / k8s).
1324
+ # - The handler runs the lease release in a worker thread with a hard
1325
+ # 1.5s join timeout so it cannot deadlock the event loop. Process
1326
+ # exits via os._exit(0) afterwards.
1327
+ # - The lifespan finally also calls _release_lease_synchronous(1.5) so
1328
+ # stdin-EOF graceful shutdown reliably releases the lease without
1329
+ # relying on a daemon thread that may die with the process.
1150
1330
 
1151
1331
 
1152
1332
  # ============================================================
@@ -1834,6 +2014,16 @@ async def lifespan(_app):
1834
2014
  wd_thread = _threading.Thread(target=_orphan_watchdog_fn, daemon=True, name="meshcode-orphan-watchdog")
1835
2015
  wd_thread.start()
1836
2016
 
2017
+ # Opt-in graceful SIGTERM handler (PROTO-LEASE-SIGTERM-RELEASE).
2018
+ # Default OFF — preserves SIG_IGN cancellation safety from run_server.
2019
+ # When MESHCODE_GRACEFUL_SIGTERM=1, the dying process releases its
2020
+ # lease before exit, eliminating the 6-8s relaunch hang under
2021
+ # supervisor-driven shutdowns (systemd / docker stop / k8s).
2022
+ try:
2023
+ _install_shutdown_signal_handlers()
2024
+ except Exception as _sig_e:
2025
+ log.debug(f"shutdown signal handler install failed: {_sig_e}")
2026
+
1837
2027
  log.info(f"lifespan started — Realtime + heartbeat thread + orphan watchdog active for {AGENT_NAME}")
1838
2028
  # Enable session recording in backend.py (hot-reloadable)
1839
2029
  try:
@@ -1895,6 +2085,16 @@ async def lifespan(_app):
1895
2085
  _threading.Thread(target=_bg_cleanup, daemon=True, name="meshcode-shutdown-cleanup").start()
1896
2086
  except Exception:
1897
2087
  pass
2088
+ # Synchronous time-boxed lease release (PROTO-LEASE-SIGTERM-RELEASE).
2089
+ # The daemon thread above can die with the process before its HTTP
2090
+ # call completes; this explicit release gives us up to 1.5s to
2091
+ # cleanly DELETE the lease row, closing the 6-8s relaunch hang
2092
+ # window under stdin-EOF graceful shutdown. Idempotent — if the
2093
+ # signal handler already ran, the global guard short-circuits this.
2094
+ try:
2095
+ _release_lease_synchronous(timeout_s=1.5)
2096
+ except Exception:
2097
+ pass
1898
2098
 
1899
2099
 
1900
2100
  # ============================================================
@@ -2544,15 +2744,29 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
2544
2744
  """Block until a mesh message arrives or a task needs attention. Loops internally; agent never decides to re-call. timeout_seconds: per-cycle cap (default+max 20)."""
2545
2745
  global _IN_WAIT, _CONSECUTIVE_IDLE_SECONDS, _LAST_SEEN_TS
2546
2746
 
2547
- # PRODUCT RULE 1: If agent has open tasks, refuse to wait. Work first.
2548
- # Exception: commander/leader agents can wait while monitoring they
2549
- # delegate tasks and need to stay in the wait loop to receive reports.
2747
+ # PROTO-WAIT-OPEN-TASKS-CONTRADICTION (task 88eb5492, 2026-05-08):
2748
+ # The previous PRODUCT RULE 1 refused wait when the agent had ANY
2749
+ # assigned-or-claimed open task but stay_on_loop.py also DEMANDS
2750
+ # that turns end with meshcode_wait. Result: agent trapped, prints
2751
+ # "Type stop." until the human types a release keyword. PROTO-MCP-
2752
+ # UNREACHABLE-RELEASE (task d2bdc974, 338c793) does not cover this
2753
+ # because MCP is alive; the server is the gate.
2754
+ #
2755
+ # New contract (Option A from task spec): tasks NEVER block wait.
2756
+ # They surface in the wait response as `pending_tasks_hint` so the
2757
+ # agent sees them in the next tick and can decide whether to claim
2758
+ # the next item or sign off. Messages are still the only blocking
2759
+ # signal — see PRODUCT RULE 2 below.
2760
+ #
2761
+ # The legacy refuse-on-open-tasks behavior is preserved behind an
2762
+ # opt-in env flag MESHCODE_WAIT_BLOCKS_ON_TASKS=1 in case any
2763
+ # workspace depends on it; default OFF to fix the contradiction.
2550
2764
  pending_tasks = _get_pending_tasks_summary()
2551
- if pending_tasks:
2765
+ if pending_tasks and os.environ.get("MESHCODE_WAIT_BLOCKS_ON_TASKS", "").lower() in ("1", "true", "yes"):
2552
2766
  if not _is_leader_agent():
2553
2767
  return {
2554
2768
  "refused": True,
2555
- "reason": "You have open tasks. Work them before entering wait.",
2769
+ "reason": "You have open tasks. Work them before entering wait. (legacy behavior, MESHCODE_WAIT_BLOCKS_ON_TASKS=1)",
2556
2770
  "pending_tasks": pending_tasks,
2557
2771
  "count": len(pending_tasks),
2558
2772
  }
@@ -3885,6 +4099,67 @@ def meshcode_auto_wake() -> Dict[str, Any]:
3885
4099
  }
3886
4100
 
3887
4101
 
4102
+ @mcp.tool()
4103
+ @with_working_status
4104
+ def meshcode_boot() -> Dict[str, Any]:
4105
+ """One-shot boot context. Replaces 5 MCP calls (check + tasks + status +
4106
+ auto_wake + recall) with a single round-trip to public.mc_boot (mig 271).
4107
+
4108
+ Returns a dict with: agent_status, inbox_messages, open_tasks_for_self,
4109
+ mesh_status, persona_hint, health_summary, top3_memory_hints_for_recent_subjects.
4110
+ Side effect: bumps last_heartbeat on the server side.
4111
+
4112
+ Recommended boot order: meshcode_set_status('online') → meshcode_boot() →
4113
+ meshcode_wait(). Old tools remain for back-compat.
4114
+ """
4115
+ api_key = _get_api_key()
4116
+ resp = be.sb_rpc("mc_boot", {
4117
+ "p_api_key": api_key,
4118
+ "p_project_id": _PROJECT_ID,
4119
+ "p_agent_name": AGENT_NAME,
4120
+ })
4121
+ if not isinstance(resp, dict) or not resp.get("ok"):
4122
+ # Soft-fall back to legacy boot-context if mc_boot is missing on
4123
+ # older projects. The CLAUDE.md template tells the LLM to switch
4124
+ # to the 5-call sequence if this returns ok=false with a deploy
4125
+ # error. Surfacing the inner envelope helps debug.
4126
+ return {
4127
+ "ok": False,
4128
+ "error": (resp.get("error") if isinstance(resp, dict) else None) or "mc_boot_unavailable",
4129
+ "fallback_hint": "Call meshcode_check + meshcode_tasks + meshcode_status + meshcode_auto_wake + meshcode_recall instead.",
4130
+ "raw": resp,
4131
+ }
4132
+
4133
+ # Update local message-tracking state from inbox so a follow-up
4134
+ # meshcode_check or meshcode_wait does not re-surface what mc_boot
4135
+ # already returned. Mirrors the bookkeeping in meshcode_check.
4136
+ global _LAST_SEEN_TS, _PERSONA, _PERSONA_INJECTED
4137
+ inbox = resp.get("inbox_messages") or []
4138
+ if inbox:
4139
+ latest_ts = max((str(m.get("ts", "") or m.get("created_at", "")) for m in inbox), default=None)
4140
+ if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
4141
+ _LAST_SEEN_TS = latest_ts
4142
+ for m in inbox:
4143
+ try:
4144
+ _mark_seen(_seen_key({
4145
+ "id": m.get("id"),
4146
+ "from": m.get("from"),
4147
+ "payload": m.get("payload") or {},
4148
+ "ts": m.get("ts"),
4149
+ }))
4150
+ except Exception:
4151
+ pass
4152
+
4153
+ # Adopt persona from RPC if the lifespan load missed it (older project).
4154
+ if not _PERSONA:
4155
+ ph = resp.get("persona_hint") or {}
4156
+ if isinstance(ph, dict) and ph:
4157
+ _PERSONA = ph
4158
+ _PERSONA_INJECTED = False # let with_working_status surface it once
4159
+
4160
+ return resp
4161
+
4162
+
3888
4163
  # ----------------- MESH LINK TOOLS -----------------
3889
4164
 
3890
4165
  @mcp.tool()
@@ -4416,7 +4691,7 @@ def meshcode_health() -> Dict[str, Any]:
4416
4691
  # ----------------- RESOURCES -----------------
4417
4692
 
4418
4693
  @mcp.tool()
4419
- def meshcode_auto_wake(enabled: bool) -> Dict[str, Any]:
4694
+ def meshcode_auto_wake_toggle(enabled: bool) -> Dict[str, Any]:
4420
4695
  """Toggle auto-wake: when enabled, if this agent receives a mesh message
4421
4696
  while idle (not in meshcode_wait), the MCP server injects a nudge into
4422
4697
  the terminal via OS automation (AppleScript on Mac, PowerShell on Windows).
@@ -946,17 +946,22 @@ If the meshcode_* tool schemas are deferred in your client, your FIRST tool
946
946
  call MUST be:
947
947
 
948
948
  ```
949
- ToolSearch(query="select:meshcode_set_status,meshcode_check,meshcode_tasks,meshcode_auto_wake,meshcode_status,meshcode_wait,meshcode_send,meshcode_task_claim,meshcode_task_complete,meshcode_remember,meshcode_recall")
949
+ ToolSearch(query="select:meshcode_set_status,meshcode_boot,meshcode_check,meshcode_tasks,meshcode_auto_wake,meshcode_status,meshcode_wait,meshcode_send,meshcode_task_claim,meshcode_task_complete,meshcode_remember,meshcode_recall")
950
950
  ```
951
951
 
952
952
  Then run the boot sequence:
953
953
 
954
954
  1. `meshcode_set_status(status="online", task="ready")`
955
- 2. `meshcode_check()` — read NEW messages (last_seen auto-restored from mesh memory)
956
- 3. `meshcode_tasks()` — see assigned/pending tasks; claim any unclaimed for this agent
957
- 4. `meshcode_auto_wake()` — scan meshwork health, surface issues
958
- 5. `meshcode_status()` see who's online
959
- 6. `meshcode_wait()` — enter the permanent loop (see next section)
955
+ 2. `meshcode_boot()` — single RPC returning inbox + tasks + mesh status + persona + health + memory hints (replaces check+tasks+status+auto_wake+recall in one round-trip; mig 271). Falls back gracefully on older projects.
956
+ 3. `meshcode_wait()` — enter the permanent loop (see next section)
957
+
958
+ Legacy boot order (only if `meshcode_boot()` returns `ok: false` with `mc_boot_unavailable`):
959
+
960
+ a. `meshcode_check()` — read NEW messages
961
+ b. `meshcode_tasks()` — see assigned/pending tasks
962
+ c. `meshcode_auto_wake()` — scan meshwork health
963
+ d. `meshcode_status()` — see who's online
964
+ e. `meshcode_wait()` — enter the loop
960
965
 
961
966
  ## PERMANENT LOOP (THE #1 RULE)
962
967
 
@@ -982,6 +987,11 @@ If `meshcode_wait()` times out, call it again with a 2× longer timeout (cap 180
982
987
  for trackable work. Keep messages <100 tokens (signals only).
983
988
  - No empty acks. JSON reports only.
984
989
  - Threading: pass `in_reply_to`.
990
+ - Multi-recipient: when the same payload goes to ≥2 agents, use
991
+ `meshcode_send(to=["a","b"])` or CSV `"a,b,c"` — never N single-sends.
992
+ Server fans out via `mc_send_multi` with a shared `group_id` and the FE
993
+ collapses the row. Saves N-1 RPCs + round-trips. Cross-mesh `agent@meshwork`
994
+ is single-recipient only (mixed lists rejected v1).
985
995
  - `sensitive=True` for secrets / PII.
986
996
  - Memory: `meshcode_remember(key, value)` for reusable learnings. Don't dump
987
997
  task summaries into memory — tasks already persist.
@@ -1007,7 +1017,7 @@ meshcode run {agent}
1007
1017
  server_id = f"meshcode-{project}-{agent}"
1008
1018
  slash_cmd_body = f"""---
1009
1019
  description: Enter the permanent MeshCode wait loop — block in meshcode_wait until a real message arrives, never idle in text mode.
1010
- allowed-tools: mcp__{server_id}__meshcode_wait, mcp__{server_id}__meshcode_check, mcp__{server_id}__meshcode_set_status, mcp__{server_id}__meshcode_send, mcp__{server_id}__meshcode_status, mcp__{server_id}__meshcode_tasks, mcp__{server_id}__meshcode_task_claim, mcp__{server_id}__meshcode_task_complete, mcp__{server_id}__meshcode_remember, mcp__{server_id}__meshcode_recall
1020
+ allowed-tools: mcp__{server_id}__meshcode_wait, mcp__{server_id}__meshcode_boot, mcp__{server_id}__meshcode_check, mcp__{server_id}__meshcode_set_status, mcp__{server_id}__meshcode_send, mcp__{server_id}__meshcode_status, mcp__{server_id}__meshcode_tasks, mcp__{server_id}__meshcode_task_claim, mcp__{server_id}__meshcode_task_complete, mcp__{server_id}__meshcode_remember, mcp__{server_id}__meshcode_recall
1011
1021
  ---
1012
1022
 
1013
1023
  # /meshcode-wait — enter the permanent loop
@@ -1031,9 +1041,12 @@ Call `meshcode_wait` now.
1031
1041
  stop_hook_body = '''#!/usr/bin/env python3
1032
1042
  """Stop hook: refuse to end the agent's turn unless one of:
1033
1043
  (a) the last user message contains a release keyword,
1034
- (b) the last assistant turn already called meshcode_wait, or
1035
- (c) the latest meshcode_wait tool_result authorized exit
1036
- (must_exit=True OR done_signals non-empty).
1044
+ (b) the last assistant turn already called meshcode_wait,
1045
+ (c) the most recent meshcode_wait tool_result authorized exit
1046
+ (must_exit=True OR done_signals non-empty), or
1047
+ (d) the MCP server itself is unreachable — errored meshcode_wait
1048
+ tool_result OR ToolSearch select:meshcode_wait returned no match
1049
+ (PROTO-MCP-UNREACHABLE-RELEASE / task d2bdc974, 2026-05-08).
1037
1050
  """
1038
1051
  import json
1039
1052
  import sys
@@ -1171,6 +1184,78 @@ def _latest_wait_result_authorizes_exit(transcript_path):
1171
1184
  return False
1172
1185
 
1173
1186
 
1187
+ _MCP_DEAD_MARKERS = (
1188
+ "no such tool",
1189
+ "connection closed",
1190
+ "no matching deferred tools found",
1191
+ "mcp server",
1192
+ "mcp error",
1193
+ )
1194
+
1195
+
1196
+ def _mcp_unreachable_recently(transcript_path, lookback_records=40):
1197
+ """Release the loop when meshcode_wait is currently unreachable.
1198
+
1199
+ A dropped MCP de-registers meshcode_wait from the runtime. Without
1200
+ this signal the agent gets trapped: the stop hook keeps demanding
1201
+ meshcode_wait while the tool itself is gone, and only a user-typed
1202
+ keyword can release. Lived through 2026-05-07 by backend after first
1203
+ wait dropped stdio for 25min.
1204
+ """
1205
+ try:
1206
+ records = []
1207
+ with transcript_path.open() as f:
1208
+ for line in f:
1209
+ try:
1210
+ records.append(json.loads(line))
1211
+ except json.JSONDecodeError:
1212
+ continue
1213
+ tail = records[-lookback_records:] if lookback_records else records
1214
+ wait_tool_use_ids = set()
1215
+ toolsearch_wait_ids = set()
1216
+ for rec in tail:
1217
+ content = rec.get("content")
1218
+ blocks = content if isinstance(content, list) else [content] if isinstance(content, dict) else []
1219
+ for block in blocks:
1220
+ if not isinstance(block, dict):
1221
+ continue
1222
+ btype = block.get("type")
1223
+ if btype == "tool_use":
1224
+ name = str(block.get("name", ""))
1225
+ use_id = block.get("id")
1226
+ if name.endswith(WAIT_TOOL_SUFFIX) and use_id:
1227
+ wait_tool_use_ids.add(use_id)
1228
+ elif name.endswith("ToolSearch") or name == "ToolSearch":
1229
+ query = ""
1230
+ inp = block.get("input")
1231
+ if isinstance(inp, dict):
1232
+ query = str(inp.get("query", ""))
1233
+ if "meshcode_wait" in query and use_id:
1234
+ toolsearch_wait_ids.add(use_id)
1235
+ elif btype == "tool_result":
1236
+ use_id = block.get("tool_use_id")
1237
+ if not use_id:
1238
+ continue
1239
+ is_error = bool(block.get("is_error"))
1240
+ raw_text = block.get("content")
1241
+ if isinstance(raw_text, list):
1242
+ raw_text = " ".join(
1243
+ p.get("text", "")
1244
+ for p in raw_text
1245
+ if isinstance(p, dict)
1246
+ )
1247
+ text_lower = str(raw_text or "").lower()
1248
+ if use_id in wait_tool_use_ids and (
1249
+ is_error or any(m in text_lower for m in _MCP_DEAD_MARKERS)
1250
+ ):
1251
+ return True
1252
+ if use_id in toolsearch_wait_ids and "no matching deferred tools found" in text_lower:
1253
+ return True
1254
+ return False
1255
+ except (OSError, FileNotFoundError):
1256
+ return False
1257
+
1258
+
1174
1259
  def main():
1175
1260
  raw = sys.stdin.read()
1176
1261
  try:
@@ -1186,6 +1271,15 @@ def main():
1186
1271
  sys.exit(0)
1187
1272
  if transcript_path and _latest_wait_result_authorizes_exit(transcript_path):
1188
1273
  sys.exit(0)
1274
+ if transcript_path and _mcp_unreachable_recently(transcript_path):
1275
+ # Log the reason so session traces can show why the loop
1276
+ # released without a user-typed release keyword.
1277
+ try:
1278
+ sys.stderr.write("[stay_on_loop] release: mcp_unreachable\\n")
1279
+ sys.stderr.flush()
1280
+ except Exception:
1281
+ pass
1282
+ sys.exit(0)
1189
1283
  print(json.dumps({
1190
1284
  "decision": "block",
1191
1285
  "reason": (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.10.100
3
+ Version: 2.10.101
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT