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.
- {meshcode-2.10.100 → meshcode-2.10.101}/PKG-INFO +1 -1
- meshcode-2.10.101/meshcode/__init__.py +82 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/server.py +309 -34
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/setup_clients.py +104 -10
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/SOURCES.txt +4 -1
- {meshcode-2.10.100 → meshcode-2.10.101}/pyproject.toml +1 -1
- meshcode-2.10.101/tests/test_lease_sigterm_release.py +299 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_rpc_migrations.py +65 -0
- meshcode-2.10.101/tests/test_stay_on_loop_hook.py +302 -0
- meshcode-2.10.101/tests/test_wait_open_tasks_contradiction.py +86 -0
- meshcode-2.10.100/meshcode/__init__.py +0 -82
- {meshcode-2.10.100 → meshcode-2.10.101}/README.md +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/cli.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/compat.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/daemon.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/invites.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/launcher.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/preferences.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/secrets.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/self_update.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode/upload.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/cli.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/compat.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/invites.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/launcher.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/preferences.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/secrets.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/self_update.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/meshcode/upload.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/scripts/sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_core.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/cli.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/compat.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/invites.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/launcher.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/preferences.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/secrets.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/self_update.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/meshcode/upload.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/scripts/sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_core.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/cli.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/compat.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/invites.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/meshcode/upload.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_core.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/setup.cfg +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_core.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.100 → meshcode-2.10.101}/tests/test_status_enum_coverage.py +0 -0
|
@@ -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
|
|
140
|
-
"""
|
|
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
|
-
|
|
147
|
-
if
|
|
148
|
-
return
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
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
|
-
#
|
|
2548
|
-
#
|
|
2549
|
-
#
|
|
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
|
|
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. `
|
|
956
|
-
3. `
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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,
|
|
1035
|
-
(c) the
|
|
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": (
|