meshcode 2.10.101__tar.gz → 2.11.1__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.101 → meshcode-2.11.1}/PKG-INFO +1 -1
- meshcode-2.11.1/meshcode/__init__.py +82 -0
- meshcode-2.11.1/meshcode/_stop_hook_template.py +514 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/comms_v4.py +9 -1
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/meshcode_mcp/backend.py +15 -4
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/meshcode_mcp/server.py +273 -14
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/run_agent.py +23 -0
- meshcode-2.11.1/meshcode/scripts/check_secrets.py +134 -0
- meshcode-2.11.1/meshcode/scripts/race_rate_harness.py +308 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/setup_clients.py +164 -260
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/SOURCES.txt +3 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/pyproject.toml +1 -1
- meshcode-2.10.101/meshcode/__init__.py +0 -82
- {meshcode-2.10.101 → meshcode-2.11.1}/README.md +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/cli.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/compat.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/daemon.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/invites.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/launcher.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/preferences.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/secrets.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/self_update.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/meshcode/upload.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/comms_v4.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-backend-wt/meshcode/__init__.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/cli.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/compat.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/invites.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/launcher.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/preferences.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/secrets.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/self_update.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/meshcode/upload.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/scripts/sentinel.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_core.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-backend-wt}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1/meshcode-backend-wt}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/__init__.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/cli.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/compat.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/invites.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/launcher.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/preferences.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/secrets.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/self_update.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt/build/lib}/meshcode/upload.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/comms_v4.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/__init__.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/cli.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-noun-wt}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/compat.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/invites.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/launcher.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-noun-wt}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-noun-wt}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/preferences.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/secrets.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/self_update.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-noun-wt}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/meshcode/upload.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/scripts/sentinel.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_core.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.101/meshcode-tasks-wt → meshcode-2.11.1/meshcode-noun-wt}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/comms_v4.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/__init__.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/cli.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/compat.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/error_hints.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/exceptions.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/invites.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/launcher.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/preferences.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/quickstart.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/secrets.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/self_update.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/supervisor.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt/build/lib → meshcode-2.11.1/meshcode-tasks-wt}/meshcode/upload.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/scripts/sentinel.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_core.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.101/meshcode-noun-wt → meshcode-2.11.1/meshcode-tasks-wt}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/setup.cfg +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_core.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_exceptions.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_security_regressions.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_sentinel.py +0 -0
- {meshcode-2.10.101/meshcode-backend-wt → meshcode-2.11.1}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.10.101 → meshcode-2.11.1}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
+
__version__ = "2.11.1"
|
|
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
|
+
]
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""Stop hook template — single source of truth for stay_on_loop.py.
|
|
2
|
+
|
|
3
|
+
Extracted from setup_clients.py 2026-05-13 to enable `meshcode patch-hooks`
|
|
4
|
+
to rewrite existing workspaces with the latest hook logic (BUG-STOP-HOOK-
|
|
5
|
+
TRAPS-ON-EXPLICIT-SLEEP-AUTH, task 60565831). Single source means future
|
|
6
|
+
hook fixes only edit this file; setup_workspace + patch-hooks both read it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
STOP_HOOK_BODY = '''#!/usr/bin/env python3
|
|
10
|
+
"""Stop hook: refuse to end the agent's turn unless one of:
|
|
11
|
+
(a) the last user message contains a release keyword,
|
|
12
|
+
(b) the last assistant turn already called meshcode_wait,
|
|
13
|
+
(c) the most recent meshcode_wait tool_result authorized exit
|
|
14
|
+
(must_exit=True OR done_signals non-empty OR a sammybenu mesh
|
|
15
|
+
message contains a release keyword in payload.text), or
|
|
16
|
+
(d) the MCP server itself is unreachable (errored wait results, or
|
|
17
|
+
ToolSearch found no meshcode_wait — agent has no way to call it).
|
|
18
|
+
|
|
19
|
+
Samuel directive 2026-04-30 — agents kept saying "en loop" without actually
|
|
20
|
+
entering meshcode_wait, so the loop only existed in chat text. This hook
|
|
21
|
+
forces the agent to make a real meshcode_wait tool call before its turn
|
|
22
|
+
can stop.
|
|
23
|
+
|
|
24
|
+
Bug-fix 2026-05-05 (condition c): mesh broadcasts and direct sleep-
|
|
25
|
+
authorization done_signals were getting blocked — the agent set
|
|
26
|
+
status=sleeping then ended the turn, but neither (a) nor (b) matched, so
|
|
27
|
+
the hook trapped the agent in an infinite block-loop. Now any explicit
|
|
28
|
+
must_exit=True or non-empty done_signals from the latest wait result
|
|
29
|
+
releases the agent.
|
|
30
|
+
|
|
31
|
+
Bug-fix 2026-05-08 (condition d, PROTO-MCP-UNREACHABLE-RELEASE, task
|
|
32
|
+
d2bdc974): when the MCP server drops mid-session, stay_on_loop kept
|
|
33
|
+
demanding meshcode_wait but the tool was de-registered → agent printed
|
|
34
|
+
"Type stop." infinitely until the human typed stop/sleep/exit/done.
|
|
35
|
+
Now we probe the transcript for evidence the tool is unreachable
|
|
36
|
+
(errored tool_result OR ToolSearch miss) and release with mcp_unreachable.
|
|
37
|
+
|
|
38
|
+
Bug-fix 2026-05-08b (condition d extended, mesh-commander launch fail):
|
|
39
|
+
the prior detection only matched if the agent (a) called meshcode_wait
|
|
40
|
+
and got an error or (b) ran ToolSearch select:meshcode_wait and got
|
|
41
|
+
"no matching deferred tools found". Failed when the agent observed MCP
|
|
42
|
+
absence via system-reminder before ever attempting either tool — Samuel
|
|
43
|
+
reported commander trapped in this exact path. Two new release paths:
|
|
44
|
+
(1) latest <system-reminder> deferred-tool listing enumerates mcp__
|
|
45
|
+
entries but ZERO meshcode_* entries → MCP not registered.
|
|
46
|
+
(2) latest user-typed message names MCP failure ("mcp server fail",
|
|
47
|
+
"mcp no esta conectado", "mcp no esta cargado", "mcp not
|
|
48
|
+
connected") → human-confirmed unreachable.
|
|
49
|
+
"""
|
|
50
|
+
import json
|
|
51
|
+
import sys
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
RELEASE_WORDS = (
|
|
55
|
+
"stop", "sleep", "exit", "quit", "done",
|
|
56
|
+
"duerme", "descansa", "dormir", "quedate dormido", "se acab",
|
|
57
|
+
"got_done",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Known human-user handles. Used as a back-compat fallback when the server
|
|
61
|
+
# hasn't stamped `sender_role="human_user"` on the message payload. Keep
|
|
62
|
+
# tight — these names get release-keyword power in mesh broadcasts.
|
|
63
|
+
# Server-side fix is to populate sender_role; this set is the safety net.
|
|
64
|
+
_KNOWN_HUMAN_HANDLES = frozenset({
|
|
65
|
+
"sammybenu", "samuel", "sam",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
WAIT_TOOL_SUFFIX = "meshcode_wait"
|
|
69
|
+
|
|
70
|
+
# Tail-window for the unreachable probe. 40 records covers the typical
|
|
71
|
+
# boot + first few wait cycles without scanning the entire transcript.
|
|
72
|
+
_UNREACHABLE_LOOKBACK = 40
|
|
73
|
+
|
|
74
|
+
# Substrings in tool_result text that indicate MCP-side death. Matched
|
|
75
|
+
# case-insensitively. Keep the list tight — false positives here
|
|
76
|
+
# release the loop early.
|
|
77
|
+
_MCP_DEAD_MARKERS = (
|
|
78
|
+
"no such tool",
|
|
79
|
+
"connection closed",
|
|
80
|
+
"no matching deferred tools found",
|
|
81
|
+
"mcp server",
|
|
82
|
+
"mcp error",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Substrings in a system-reminder that indicate a deferred-tool listing
|
|
86
|
+
# is being shown to the agent. Used to find the latest tool-availability
|
|
87
|
+
# snapshot in the transcript.
|
|
88
|
+
_DEFERRED_LISTING_MARKERS = (
|
|
89
|
+
"the following deferred tools are now available",
|
|
90
|
+
"deferred tools are now available via toolsearch",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Substrings in a user-typed message indicating the human observed MCP
|
|
94
|
+
# failure. All matched case-insensitively; presence of any → release.
|
|
95
|
+
# Kept tight: each phrase explicitly names MCP + a failure verb so we
|
|
96
|
+
# don't false-positive on casual mentions.
|
|
97
|
+
_USER_MCP_FAIL_PHRASES = (
|
|
98
|
+
"mcp server fail",
|
|
99
|
+
"mcp server failed",
|
|
100
|
+
"mcp no esta conectado",
|
|
101
|
+
"mcp no está conectado",
|
|
102
|
+
"mcp no esta cargado",
|
|
103
|
+
"mcp no está cargado",
|
|
104
|
+
"mcp not connected",
|
|
105
|
+
"mcp failed at launch",
|
|
106
|
+
"mcp unreachable",
|
|
107
|
+
"meshcode mcp not",
|
|
108
|
+
"el mcp de meshcode no",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _last_user_message(transcript_path):
|
|
113
|
+
try:
|
|
114
|
+
last = ""
|
|
115
|
+
with transcript_path.open() as f:
|
|
116
|
+
for line in f:
|
|
117
|
+
try:
|
|
118
|
+
rec = json.loads(line)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
continue
|
|
121
|
+
if rec.get("role") == "user":
|
|
122
|
+
content = rec.get("content")
|
|
123
|
+
if isinstance(content, str):
|
|
124
|
+
last = content
|
|
125
|
+
elif isinstance(content, list):
|
|
126
|
+
# Skip tool_result-only user turns; we want the human's text.
|
|
127
|
+
text_parts = [
|
|
128
|
+
p.get("text", "")
|
|
129
|
+
for p in content
|
|
130
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
131
|
+
]
|
|
132
|
+
if text_parts:
|
|
133
|
+
last = " ".join(text_parts)
|
|
134
|
+
return last
|
|
135
|
+
except (OSError, FileNotFoundError):
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _last_assistant_turn_called_wait(transcript_path):
|
|
140
|
+
try:
|
|
141
|
+
records = []
|
|
142
|
+
with transcript_path.open() as f:
|
|
143
|
+
for line in f:
|
|
144
|
+
try:
|
|
145
|
+
records.append(json.loads(line))
|
|
146
|
+
except json.JSONDecodeError:
|
|
147
|
+
continue
|
|
148
|
+
last_assistant_blocks = []
|
|
149
|
+
for rec in reversed(records):
|
|
150
|
+
if rec.get("role") == "user":
|
|
151
|
+
if last_assistant_blocks:
|
|
152
|
+
break
|
|
153
|
+
continue
|
|
154
|
+
if rec.get("role") == "assistant":
|
|
155
|
+
content = rec.get("content")
|
|
156
|
+
if isinstance(content, list):
|
|
157
|
+
last_assistant_blocks.extend(content)
|
|
158
|
+
elif isinstance(content, dict):
|
|
159
|
+
last_assistant_blocks.append(content)
|
|
160
|
+
for block in last_assistant_blocks:
|
|
161
|
+
if not isinstance(block, dict):
|
|
162
|
+
continue
|
|
163
|
+
if block.get("type") == "tool_use":
|
|
164
|
+
name = str(block.get("name", ""))
|
|
165
|
+
if name.endswith(WAIT_TOOL_SUFFIX):
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
168
|
+
except (OSError, FileNotFoundError):
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _latest_wait_result_authorizes_exit(transcript_path):
|
|
173
|
+
"""Walk transcript backwards. Find the most recent meshcode_wait
|
|
174
|
+
tool_use → match its tool_result by tool_use_id → if the parsed
|
|
175
|
+
result has must_exit=True or non-empty done_signals, return True.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
records = []
|
|
179
|
+
with transcript_path.open() as f:
|
|
180
|
+
for line in f:
|
|
181
|
+
try:
|
|
182
|
+
records.append(json.loads(line))
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
wait_use_ids = set()
|
|
187
|
+
for rec in records:
|
|
188
|
+
if rec.get("role") != "assistant":
|
|
189
|
+
continue
|
|
190
|
+
content = rec.get("content")
|
|
191
|
+
blocks = content if isinstance(content, list) else [content] if isinstance(content, dict) else []
|
|
192
|
+
for block in blocks:
|
|
193
|
+
if not isinstance(block, dict):
|
|
194
|
+
continue
|
|
195
|
+
if block.get("type") == "tool_use" and str(block.get("name", "")).endswith(WAIT_TOOL_SUFFIX):
|
|
196
|
+
use_id = block.get("id")
|
|
197
|
+
if use_id:
|
|
198
|
+
wait_use_ids.add(use_id)
|
|
199
|
+
|
|
200
|
+
if not wait_use_ids:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
for rec in reversed(records):
|
|
204
|
+
if rec.get("role") != "user":
|
|
205
|
+
continue
|
|
206
|
+
content = rec.get("content")
|
|
207
|
+
blocks = content if isinstance(content, list) else [content] if isinstance(content, dict) else []
|
|
208
|
+
for block in blocks:
|
|
209
|
+
if not isinstance(block, dict):
|
|
210
|
+
continue
|
|
211
|
+
if block.get("type") != "tool_result":
|
|
212
|
+
continue
|
|
213
|
+
if block.get("tool_use_id") not in wait_use_ids:
|
|
214
|
+
continue
|
|
215
|
+
inner = block.get("content")
|
|
216
|
+
text = ""
|
|
217
|
+
if isinstance(inner, str):
|
|
218
|
+
text = inner
|
|
219
|
+
elif isinstance(inner, list):
|
|
220
|
+
text = " ".join(
|
|
221
|
+
p.get("text", "") for p in inner
|
|
222
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
223
|
+
)
|
|
224
|
+
if not text:
|
|
225
|
+
continue
|
|
226
|
+
try:
|
|
227
|
+
parsed = json.loads(text)
|
|
228
|
+
except json.JSONDecodeError:
|
|
229
|
+
continue
|
|
230
|
+
payload = parsed.get("result", parsed) if isinstance(parsed, dict) else parsed
|
|
231
|
+
if not isinstance(payload, dict):
|
|
232
|
+
continue
|
|
233
|
+
if payload.get("must_exit") is True:
|
|
234
|
+
return True
|
|
235
|
+
signals = payload.get("done_signals")
|
|
236
|
+
if isinstance(signals, list) and signals:
|
|
237
|
+
return True
|
|
238
|
+
# Condition (e) — 2026-05-08, extended 2026-05-13: a mesh
|
|
239
|
+
# broadcast / message from a HUMAN USER containing a release
|
|
240
|
+
# keyword counts as a sleep authorization. Without this branch,
|
|
241
|
+
# `meshcode_broadcast("pueden exit y dormir")` leaves the agent
|
|
242
|
+
# trapped because the keyword never entered the Claude Code
|
|
243
|
+
# transcript as user-typed text.
|
|
244
|
+
#
|
|
245
|
+
# 2026-05-13 (BUG-STOP-HOOK-TRAPS-ON-EXPLICIT-SLEEP-AUTH,
|
|
246
|
+
# task 60565831): widened beyond the hardcoded "sammybenu"
|
|
247
|
+
# check. wren@justforfun was trapped because Samuel's mesh
|
|
248
|
+
# handle in justforfun isn't "sammybenu". New scoping:
|
|
249
|
+
# (1) sender_role == "human_user" (server-stamped, forward-compat)
|
|
250
|
+
# (2) sender name in known-human handle set (back-compat)
|
|
251
|
+
# Both are scoped to release keywords only — same threat
|
|
252
|
+
# surface as before, broader cover for new meshes.
|
|
253
|
+
msgs = payload.get("messages") or []
|
|
254
|
+
if isinstance(msgs, list):
|
|
255
|
+
for m in msgs:
|
|
256
|
+
if not isinstance(m, dict):
|
|
257
|
+
continue
|
|
258
|
+
sender = m.get("from", "")
|
|
259
|
+
m_payload = m.get("payload") or {}
|
|
260
|
+
sender_role = None
|
|
261
|
+
if isinstance(m_payload, dict):
|
|
262
|
+
sender_role = m_payload.get("sender_role") or m_payload.get("from_role")
|
|
263
|
+
is_human = (
|
|
264
|
+
sender_role == "human_user"
|
|
265
|
+
or sender in _KNOWN_HUMAN_HANDLES
|
|
266
|
+
)
|
|
267
|
+
if not is_human:
|
|
268
|
+
continue
|
|
269
|
+
m_text = ""
|
|
270
|
+
if isinstance(m_payload, dict):
|
|
271
|
+
m_text = str(m_payload.get("text", "")).lower()
|
|
272
|
+
elif isinstance(m_payload, str):
|
|
273
|
+
m_text = m_payload.lower()
|
|
274
|
+
if any(word in m_text for word in RELEASE_WORDS):
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
return False
|
|
278
|
+
except (OSError, FileNotFoundError):
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _mcp_unreachable_recently(transcript_path, lookback_records=_UNREACHABLE_LOOKBACK):
|
|
283
|
+
"""Detect that MCP `meshcode_wait` is currently unreachable.
|
|
284
|
+
|
|
285
|
+
Without this signal, a dead MCP traps the agent: the stop hook keeps
|
|
286
|
+
demanding meshcode_wait while the tool itself has been de-registered
|
|
287
|
+
by the runtime, and the agent has no way to satisfy it short of the
|
|
288
|
+
user typing a release keyword. Lived through 2026-05-07 by backend
|
|
289
|
+
after first wait dropped stdio.
|
|
290
|
+
|
|
291
|
+
Three classifications, all → release:
|
|
292
|
+
1. registered+errored — recent meshcode_wait tool_result has
|
|
293
|
+
is_error=True OR contains an _MCP_DEAD_MARKERS substring.
|
|
294
|
+
2. unregistered — agent ran ToolSearch select:meshcode_wait and
|
|
295
|
+
got "no matching deferred tools found".
|
|
296
|
+
3. (current implementation) any of the above wins; we do NOT
|
|
297
|
+
require a count threshold because a single tool-down event
|
|
298
|
+
is high-signal: meshcode_wait should never legitimately fail
|
|
299
|
+
except via must_exit/done_signals which are handled in (c).
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
records = []
|
|
303
|
+
with transcript_path.open() as f:
|
|
304
|
+
for line in f:
|
|
305
|
+
try:
|
|
306
|
+
records.append(json.loads(line))
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
continue
|
|
309
|
+
tail = records[-lookback_records:] if lookback_records else records
|
|
310
|
+
wait_tool_use_ids = set()
|
|
311
|
+
toolsearch_wait_ids = set()
|
|
312
|
+
for rec in tail:
|
|
313
|
+
content = rec.get("content")
|
|
314
|
+
blocks = content if isinstance(content, list) else [content] if isinstance(content, dict) else []
|
|
315
|
+
for block in blocks:
|
|
316
|
+
if not isinstance(block, dict):
|
|
317
|
+
continue
|
|
318
|
+
btype = block.get("type")
|
|
319
|
+
if btype == "tool_use":
|
|
320
|
+
name = str(block.get("name", ""))
|
|
321
|
+
use_id = block.get("id")
|
|
322
|
+
if name.endswith(WAIT_TOOL_SUFFIX) and use_id:
|
|
323
|
+
wait_tool_use_ids.add(use_id)
|
|
324
|
+
elif name.endswith("ToolSearch") or name == "ToolSearch":
|
|
325
|
+
query = ""
|
|
326
|
+
inp = block.get("input")
|
|
327
|
+
if isinstance(inp, dict):
|
|
328
|
+
query = str(inp.get("query", ""))
|
|
329
|
+
if "meshcode_wait" in query and use_id:
|
|
330
|
+
toolsearch_wait_ids.add(use_id)
|
|
331
|
+
elif btype == "tool_result":
|
|
332
|
+
use_id = block.get("tool_use_id")
|
|
333
|
+
if not use_id:
|
|
334
|
+
continue
|
|
335
|
+
is_error = bool(block.get("is_error"))
|
|
336
|
+
raw_text = block.get("content")
|
|
337
|
+
if isinstance(raw_text, list):
|
|
338
|
+
raw_text = " ".join(
|
|
339
|
+
p.get("text", "")
|
|
340
|
+
for p in raw_text
|
|
341
|
+
if isinstance(p, dict)
|
|
342
|
+
)
|
|
343
|
+
text_lower = str(raw_text or "").lower()
|
|
344
|
+
if use_id in wait_tool_use_ids and (
|
|
345
|
+
is_error or any(m in text_lower for m in _MCP_DEAD_MARKERS)
|
|
346
|
+
):
|
|
347
|
+
return True
|
|
348
|
+
if use_id in toolsearch_wait_ids and "no matching deferred tools found" in text_lower:
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
except (OSError, FileNotFoundError):
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _latest_deferred_listing_lacks_meshcode(transcript_path, lookback_records=_UNREACHABLE_LOOKBACK):
|
|
356
|
+
"""Find the most recent <system-reminder> that enumerates deferred
|
|
357
|
+
tools. If it lists mcp__ entries (proving the listing format is
|
|
358
|
+
canonical) but ZERO meshcode_* entries, the meshcode MCP server is
|
|
359
|
+
not registered for this session. Release.
|
|
360
|
+
|
|
361
|
+
Note: the listing is delivered as user-role content, often inside a
|
|
362
|
+
<system-reminder>...</system-reminder> block but sometimes plain.
|
|
363
|
+
We match on the marker substrings rather than tag parsing.
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
records = []
|
|
367
|
+
with transcript_path.open() as f:
|
|
368
|
+
for line in f:
|
|
369
|
+
try:
|
|
370
|
+
records.append(json.loads(line))
|
|
371
|
+
except json.JSONDecodeError:
|
|
372
|
+
continue
|
|
373
|
+
tail = records[-lookback_records:] if lookback_records else records
|
|
374
|
+
for rec in reversed(tail):
|
|
375
|
+
if rec.get("role") != "user":
|
|
376
|
+
continue
|
|
377
|
+
content = rec.get("content")
|
|
378
|
+
blocks = content if isinstance(content, list) else [content] if isinstance(content, dict) else [content]
|
|
379
|
+
for block in blocks:
|
|
380
|
+
if isinstance(block, str):
|
|
381
|
+
text = block
|
|
382
|
+
elif isinstance(block, dict):
|
|
383
|
+
if block.get("type") == "tool_result":
|
|
384
|
+
# tool_result blocks aren't system-reminders; skip
|
|
385
|
+
continue
|
|
386
|
+
text = str(block.get("text", "") or block.get("content", "") or "")
|
|
387
|
+
else:
|
|
388
|
+
continue
|
|
389
|
+
lower = text.lower()
|
|
390
|
+
if not any(m in lower for m in _DEFERRED_LISTING_MARKERS):
|
|
391
|
+
continue
|
|
392
|
+
# Found a listing. Check if it enumerates real tools.
|
|
393
|
+
has_mcp = "mcp__" in lower
|
|
394
|
+
has_meshcode = "meshcode_" in lower
|
|
395
|
+
if has_mcp and not has_meshcode:
|
|
396
|
+
return True
|
|
397
|
+
# First listing that contains meshcode_ is enough to
|
|
398
|
+
# confirm MCP IS registered — stop searching backwards.
|
|
399
|
+
if has_meshcode:
|
|
400
|
+
return False
|
|
401
|
+
return False
|
|
402
|
+
except (OSError, FileNotFoundError):
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _user_explicitly_reports_mcp_failure(transcript_path, lookback_records=_UNREACHABLE_LOOKBACK):
|
|
407
|
+
"""Scan recent user-typed text for explicit MCP-failure reports.
|
|
408
|
+
Distinct from `_last_user_message`: that helper checks for release
|
|
409
|
+
KEYWORDS (stop/sleep/etc.); this one checks for human-narrated MCP
|
|
410
|
+
breakage so the agent can release even when the human didn't think
|
|
411
|
+
to type a release keyword.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
records = []
|
|
415
|
+
with transcript_path.open() as f:
|
|
416
|
+
for line in f:
|
|
417
|
+
try:
|
|
418
|
+
records.append(json.loads(line))
|
|
419
|
+
except json.JSONDecodeError:
|
|
420
|
+
continue
|
|
421
|
+
tail = records[-lookback_records:] if lookback_records else records
|
|
422
|
+
for rec in reversed(tail):
|
|
423
|
+
if rec.get("role") != "user":
|
|
424
|
+
continue
|
|
425
|
+
content = rec.get("content")
|
|
426
|
+
text = ""
|
|
427
|
+
if isinstance(content, str):
|
|
428
|
+
text = content
|
|
429
|
+
elif isinstance(content, list):
|
|
430
|
+
parts = []
|
|
431
|
+
for p in content:
|
|
432
|
+
if isinstance(p, dict) and p.get("type") == "text":
|
|
433
|
+
parts.append(str(p.get("text", "")))
|
|
434
|
+
text = " ".join(parts)
|
|
435
|
+
if not text:
|
|
436
|
+
continue
|
|
437
|
+
lower = text.lower()
|
|
438
|
+
if any(p in lower for p in _USER_MCP_FAIL_PHRASES):
|
|
439
|
+
return True
|
|
440
|
+
return False
|
|
441
|
+
except (OSError, FileNotFoundError):
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def main():
|
|
446
|
+
raw = sys.stdin.read()
|
|
447
|
+
try:
|
|
448
|
+
payload = json.loads(raw) if raw else {}
|
|
449
|
+
except json.JSONDecodeError:
|
|
450
|
+
payload = {}
|
|
451
|
+
|
|
452
|
+
transcript = payload.get("transcript_path") or payload.get("transcriptPath")
|
|
453
|
+
transcript_path = Path(transcript) if transcript else None
|
|
454
|
+
|
|
455
|
+
last_user = _last_user_message(transcript_path).lower() if transcript_path else ""
|
|
456
|
+
if any(word in last_user for word in RELEASE_WORDS):
|
|
457
|
+
sys.exit(0)
|
|
458
|
+
|
|
459
|
+
if transcript_path and _last_assistant_turn_called_wait(transcript_path):
|
|
460
|
+
sys.exit(0)
|
|
461
|
+
|
|
462
|
+
if transcript_path and _latest_wait_result_authorizes_exit(transcript_path):
|
|
463
|
+
sys.exit(0)
|
|
464
|
+
|
|
465
|
+
if transcript_path and _mcp_unreachable_recently(transcript_path):
|
|
466
|
+
# PROTO-MCP-UNREACHABLE-RELEASE: log the reason to stderr so
|
|
467
|
+
# session traces can show why the loop released without a user
|
|
468
|
+
# release keyword. Released without printing a block decision.
|
|
469
|
+
try:
|
|
470
|
+
sys.stderr.write("[stay_on_loop] release: mcp_unreachable\n")
|
|
471
|
+
sys.stderr.flush()
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
sys.exit(0)
|
|
475
|
+
|
|
476
|
+
if transcript_path and _latest_deferred_listing_lacks_meshcode(transcript_path):
|
|
477
|
+
try:
|
|
478
|
+
sys.stderr.write("[stay_on_loop] release: mcp_not_registered (deferred listing has no meshcode_*)\n")
|
|
479
|
+
sys.stderr.flush()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
sys.exit(0)
|
|
483
|
+
|
|
484
|
+
if transcript_path and _user_explicitly_reports_mcp_failure(transcript_path):
|
|
485
|
+
try:
|
|
486
|
+
sys.stderr.write("[stay_on_loop] release: user_reported_mcp_failure\n")
|
|
487
|
+
sys.stderr.flush()
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
sys.exit(0)
|
|
491
|
+
|
|
492
|
+
print(
|
|
493
|
+
json.dumps(
|
|
494
|
+
{
|
|
495
|
+
"decision": "block",
|
|
496
|
+
"reason": (
|
|
497
|
+
"Stay-on-loop: you ended your turn without calling "
|
|
498
|
+
"meshcode_wait. Per Samuel's standing rule, every turn "
|
|
499
|
+
"ends with an active meshcode_wait. Call meshcode_wait "
|
|
500
|
+
"now (timeout_seconds=20). Do not reply with text — "
|
|
501
|
+
"just make the tool call. The only way out is the user "
|
|
502
|
+
"typing stop / sleep / exit / done, a sleep "
|
|
503
|
+
"authorization (must_exit / done_signals) from the "
|
|
504
|
+
"mesh, or MCP becoming unreachable (auto-released)."
|
|
505
|
+
),
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
sys.exit(0)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == "__main__":
|
|
513
|
+
main()
|
|
514
|
+
'''
|
|
@@ -3505,6 +3505,14 @@ if __name__ == "__main__":
|
|
|
3505
3505
|
_upl = importlib.import_module("meshcode.upload")
|
|
3506
3506
|
sys.exit(_upl.cmd_upload(pos[0]))
|
|
3507
3507
|
|
|
3508
|
+
elif cmd == "patch-hooks":
|
|
3509
|
+
# meshcode patch-hooks [--dry-run]
|
|
3510
|
+
# Rewrite stay_on_loop.py in every local workspace to the latest
|
|
3511
|
+
# template. Use after a hook fix lands without rerunning full setup.
|
|
3512
|
+
import importlib
|
|
3513
|
+
_sc = importlib.import_module("meshcode.setup_clients")
|
|
3514
|
+
sys.exit(_sc.patch_hooks(dry_run=("--dry-run" in flags)))
|
|
3515
|
+
|
|
3508
3516
|
elif cmd in ("help", "--help", "-h"):
|
|
3509
3517
|
show_help()
|
|
3510
3518
|
|
|
@@ -3541,7 +3549,7 @@ if __name__ == "__main__":
|
|
|
3541
3549
|
"setup", "run", "go", "invite", "join", "invites", "members",
|
|
3542
3550
|
"revoke-invite", "revoke-member", "login", "prefs", "launcher",
|
|
3543
3551
|
"help", "init", "doctor", "compat", "upgrade", "profile", "validate-sessions", "wake-headless",
|
|
3544
|
-
"supervisor", "upload", "quickstart",
|
|
3552
|
+
"supervisor", "upload", "quickstart", "patch-hooks",
|
|
3545
3553
|
]
|
|
3546
3554
|
# Simple fuzzy: prefix match + Levenshtein-like best match
|
|
3547
3555
|
suggestions = [c for c in known_cmds if c.startswith(cmd)]
|
|
@@ -560,11 +560,17 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
560
560
|
if not project_id:
|
|
561
561
|
return {"error": f"Project '{project}' not found"}
|
|
562
562
|
|
|
563
|
+
# status="needs_launch" (BUG-NEW-AGENT-DEFAULT-STATUS-MISLEADS,
|
|
564
|
+
# 2026-05-13): a freshly-registered agent has no running MCP session
|
|
565
|
+
# yet. Treat needs_launch as the transitional state; the agent's own
|
|
566
|
+
# MCP boot will flip to 'idle' via _flip_status() on startup (see
|
|
567
|
+
# server.py L807), and the @with_working_status decorator subsequently
|
|
568
|
+
# flips to 'working' on each tool call.
|
|
563
569
|
params = {
|
|
564
570
|
"p_project_id": project_id,
|
|
565
571
|
"p_name": name,
|
|
566
572
|
"p_role": role,
|
|
567
|
-
"p_status": "
|
|
573
|
+
"p_status": "needs_launch",
|
|
568
574
|
}
|
|
569
575
|
if api_key:
|
|
570
576
|
params["p_api_key"] = api_key
|
|
@@ -573,7 +579,12 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
573
579
|
if not result or is_error(result):
|
|
574
580
|
return {"error": get_error_message(result) or "Failed to register agent"}
|
|
575
581
|
|
|
576
|
-
#
|
|
582
|
+
# Seed task only — NOT last_heartbeat. Dashboard "needs_launch" CTA
|
|
583
|
+
# (BUG-NEW-AGENT-DEFAULT-STATUS-MISLEADS) relies on last_heartbeat
|
|
584
|
+
# remaining NULL until the agent's MCP session writes a real one. If
|
|
585
|
+
# the underlying default now() populated it on INSERT, the dashboard
|
|
586
|
+
# render path treats needs_launch state as the source of truth, so
|
|
587
|
+
# NULL vs default-now() doesn't change UX (just FE's tooltip detail).
|
|
577
588
|
_ak = api_key or _get_api_key()
|
|
578
589
|
_agent_updated = False
|
|
579
590
|
if _ak:
|
|
@@ -581,7 +592,7 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
581
592
|
"p_api_key": _ak,
|
|
582
593
|
"p_project_id": project_id,
|
|
583
594
|
"p_agent_name": name,
|
|
584
|
-
"p_fields": {"task": role
|
|
595
|
+
"p_fields": {"task": role},
|
|
585
596
|
})
|
|
586
597
|
if isinstance(_upd, dict) and _upd.get("ok"):
|
|
587
598
|
_agent_updated = True
|
|
@@ -589,7 +600,7 @@ def register_agent(project: str, name: str, role: str = "", api_key: Optional[st
|
|
|
589
600
|
# Fallback: direct PATCH (gradual rollout)
|
|
590
601
|
sb_update("mc_agents",
|
|
591
602
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
592
|
-
{"task": role
|
|
603
|
+
{"task": role})
|
|
593
604
|
|
|
594
605
|
return {
|
|
595
606
|
"registered": True,
|