meshcode 2.11.107__tar.gz → 2.11.109__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.11.107 → meshcode-2.11.109}/PKG-INFO +1 -1
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/__init__.py +1 -1
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/hostd.py +163 -2
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/server.py +13 -18
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/protocol_handler.py +141 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/run_agent.py +31 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/self_update.py +50 -7
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/setup_clients.py +5 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/PKG-INFO +1 -1
- meshcode-2.11.109/meshcode.egg-info/SOURCES.txt +97 -0
- meshcode-2.11.109/meshcode.egg-info/top_level.txt +1 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/pyproject.toml +2 -2
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_wait_open_tasks_contradiction.py +3 -2
- meshcode-2.11.107/meshcode/cli.py +0 -42
- meshcode-2.11.107/meshcode/compat.py +0 -174
- meshcode-2.11.107/meshcode/error_hints.py +0 -74
- meshcode-2.11.107/meshcode/exceptions.py +0 -52
- meshcode-2.11.107/meshcode/invites.py +0 -406
- meshcode-2.11.107/meshcode/launcher.py +0 -353
- meshcode-2.11.107/meshcode/launcher_install.py +0 -414
- meshcode-2.11.107/meshcode/meshcode_mcp/__init__.py +0 -22
- meshcode-2.11.107/meshcode/meshcode_mcp/__main__.py +0 -62
- meshcode-2.11.107/meshcode/meshcode_mcp/test_backend.py +0 -86
- meshcode-2.11.107/meshcode/meshcode_mcp/test_realtime.py +0 -95
- meshcode-2.11.107/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
- meshcode-2.11.107/meshcode/preferences.py +0 -260
- meshcode-2.11.107/meshcode/protocol_v2.py +0 -129
- meshcode-2.11.107/meshcode/secrets.py +0 -365
- meshcode-2.11.107/meshcode/supervisor.py +0 -186
- meshcode-2.11.107/meshcode/upload.py +0 -125
- meshcode-2.11.107/meshcode-backend-wt/comms_v4.py +0 -1941
- meshcode-2.11.107/meshcode-backend-wt/meshcode/__init__.py +0 -82
- meshcode-2.11.107/meshcode-backend-wt/meshcode/ascii_art.py +0 -638
- meshcode-2.11.107/meshcode-backend-wt/meshcode/comms_v4.py +0 -3563
- meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -1261
- meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -460
- meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -4117
- meshcode-2.11.107/meshcode-backend-wt/meshcode/quickstart.py +0 -148
- meshcode-2.11.107/meshcode-backend-wt/meshcode/run_agent.py +0 -958
- meshcode-2.11.107/meshcode-backend-wt/meshcode/self_update.py +0 -345
- meshcode-2.11.107/meshcode-backend-wt/meshcode/setup_clients.py +0 -926
- meshcode-2.11.107/meshcode-backend-wt/scripts/sentinel.py +0 -257
- meshcode-2.11.107/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -387
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -82
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -638
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -42
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -3563
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -174
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -74
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -52
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -406
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -353
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -414
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -22
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -62
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -1261
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -460
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -4117
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -86
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -95
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -260
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -129
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -148
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -958
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -365
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -345
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -926
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -186
- meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -125
- meshcode-2.11.107/meshcode-noun-wt/comms_v4.py +0 -1941
- meshcode-2.11.107/meshcode-noun-wt/meshcode/__init__.py +0 -82
- meshcode-2.11.107/meshcode-noun-wt/meshcode/ascii_art.py +0 -638
- meshcode-2.11.107/meshcode-noun-wt/meshcode/cli.py +0 -42
- meshcode-2.11.107/meshcode-noun-wt/meshcode/comms_v4.py +0 -3563
- meshcode-2.11.107/meshcode-noun-wt/meshcode/compat.py +0 -174
- meshcode-2.11.107/meshcode-noun-wt/meshcode/error_hints.py +0 -74
- meshcode-2.11.107/meshcode-noun-wt/meshcode/exceptions.py +0 -52
- meshcode-2.11.107/meshcode-noun-wt/meshcode/invites.py +0 -406
- meshcode-2.11.107/meshcode-noun-wt/meshcode/launcher.py +0 -353
- meshcode-2.11.107/meshcode-noun-wt/meshcode/launcher_install.py +0 -414
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -22
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -62
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -1261
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -460
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -4117
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
- meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
- meshcode-2.11.107/meshcode-noun-wt/meshcode/preferences.py +0 -260
- meshcode-2.11.107/meshcode-noun-wt/meshcode/protocol_v2.py +0 -129
- meshcode-2.11.107/meshcode-noun-wt/meshcode/quickstart.py +0 -148
- meshcode-2.11.107/meshcode-noun-wt/meshcode/run_agent.py +0 -958
- meshcode-2.11.107/meshcode-noun-wt/meshcode/secrets.py +0 -365
- meshcode-2.11.107/meshcode-noun-wt/meshcode/self_update.py +0 -345
- meshcode-2.11.107/meshcode-noun-wt/meshcode/setup_clients.py +0 -926
- meshcode-2.11.107/meshcode-noun-wt/meshcode/supervisor.py +0 -186
- meshcode-2.11.107/meshcode-noun-wt/meshcode/upload.py +0 -125
- meshcode-2.11.107/meshcode-noun-wt/scripts/sentinel.py +0 -257
- meshcode-2.11.107/meshcode-noun-wt/tests/test_core.py +0 -216
- meshcode-2.11.107/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -366
- meshcode-2.11.107/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -361
- meshcode-2.11.107/meshcode-noun-wt/tests/test_exceptions.py +0 -107
- meshcode-2.11.107/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -200
- meshcode-2.11.107/meshcode-noun-wt/tests/test_migration_integrity.py +0 -176
- meshcode-2.11.107/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -236
- meshcode-2.11.107/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -255
- meshcode-2.11.107/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -387
- meshcode-2.11.107/meshcode-noun-wt/tests/test_security_regressions.py +0 -171
- meshcode-2.11.107/meshcode-noun-wt/tests/test_sentinel.py +0 -148
- meshcode-2.11.107/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -231
- meshcode-2.11.107/meshcode-tasks-wt/comms_v4.py +0 -1941
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/__init__.py +0 -82
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/ascii_art.py +0 -638
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/cli.py +0 -42
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/comms_v4.py +0 -3563
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/compat.py +0 -174
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/error_hints.py +0 -74
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/exceptions.py +0 -52
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/invites.py +0 -406
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/launcher.py +0 -353
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/launcher_install.py +0 -414
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -22
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -62
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -1261
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -460
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -4117
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/preferences.py +0 -260
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -129
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/quickstart.py +0 -148
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/run_agent.py +0 -958
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/secrets.py +0 -365
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/self_update.py +0 -345
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/setup_clients.py +0 -926
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/supervisor.py +0 -186
- meshcode-2.11.107/meshcode-tasks-wt/meshcode/upload.py +0 -125
- meshcode-2.11.107/meshcode-tasks-wt/scripts/sentinel.py +0 -257
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_core.py +0 -216
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -366
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -361
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_exceptions.py +0 -107
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -200
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -176
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -236
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -255
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -387
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_security_regressions.py +0 -171
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_sentinel.py +0 -148
- meshcode-2.11.107/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -231
- meshcode-2.11.107/meshcode.egg-info/PKG-INFO 2 +0 -460
- meshcode-2.11.107/meshcode.egg-info/SOURCES 2.txt +0 -239
- meshcode-2.11.107/meshcode.egg-info/SOURCES.txt +0 -253
- meshcode-2.11.107/meshcode.egg-info/dependency_links 2.txt +0 -1
- meshcode-2.11.107/meshcode.egg-info/entry_points 2.txt +0 -3
- meshcode-2.11.107/meshcode.egg-info/requires 2.txt +0 -13
- meshcode-2.11.107/meshcode.egg-info/top_level 2.txt +0 -4
- meshcode-2.11.107/meshcode.egg-info/top_level.txt +0 -4
- meshcode-2.11.107/tests/test_core.py +0 -216
- meshcode-2.11.107/tests/test_cross_agent_messaging.py +0 -366
- meshcode-2.11.107/tests/test_esc_deaf_state.py +0 -361
- meshcode-2.11.107/tests/test_exceptions.py +0 -107
- meshcode-2.11.107/tests/test_mark_read_batch.py +0 -200
- meshcode-2.11.107/tests/test_migration_integrity.py +0 -176
- meshcode-2.11.107/tests/test_realtime_event_freshness.py +0 -236
- meshcode-2.11.107/tests/test_rls_cross_tenant.py +0 -255
- meshcode-2.11.107/tests/test_security_regressions.py +0 -171
- meshcode-2.11.107/tests/test_sentinel.py +0 -148
- meshcode-2.11.107/tests/test_status_enum_coverage.py +0 -231
- {meshcode-2.11.107 → meshcode-2.11.109}/README.md +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/__main__.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template 2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template 3.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update 2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update 3.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/cli.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/compat.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/daemon.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/doctor.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/hostd 2.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/invites.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/launcher.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/preferences.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/secrets.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/up 2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/up.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/upload.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/setup.cfg +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 2.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 3.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_core.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_doctor.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_stay_on_loop_hook.py +0 -0
|
@@ -503,6 +503,48 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
503
503
|
# only guard; for crash respawns it's a harmless backstop (the server
|
|
504
504
|
# rate-limit at mig404 is stricter and trips first).
|
|
505
505
|
_target = f"{proj}/{agent}"
|
|
506
|
+
# CONVERGENCE GUARD (451d33a0) — recycle fast-path ONLY. Stops the env-mismatch
|
|
507
|
+
# recycle-RESPAWN storm the version-recycle guard can't see (see _recycle_blocked).
|
|
508
|
+
# Counts consecutive recycle-respawns of this target; at _RECYREC_MAX within the
|
|
509
|
+
# window it blocks relaunch + alerts once, instead of storming terminals forever.
|
|
510
|
+
if _is_recycle:
|
|
511
|
+
_rst = _load_state()
|
|
512
|
+
_rblk = _recycle_blocked(_rst, _target)
|
|
513
|
+
if _rblk:
|
|
514
|
+
_log(f"SKIP recycle-respawn {_target}: BLOCKED ({_rblk}) — non-converging recycle; "
|
|
515
|
+
f"holding relaunch (auto-retry after {_RECYREC_BLOCK_TTL_S}s or a manual Start).")
|
|
516
|
+
continue
|
|
517
|
+
_now = time.time()
|
|
518
|
+
_rrall = dict(_rst.get("recyrespawn") or {})
|
|
519
|
+
_rr = dict(_rrall.get(_target) or {})
|
|
520
|
+
if _rr.get("last_ts") and (_now - float(_rr["last_ts"])) <= _RECYREC_WINDOW_S:
|
|
521
|
+
_rr["count"] = int(_rr.get("count", 0)) + 1
|
|
522
|
+
else:
|
|
523
|
+
_rr = {"count": 1} # window elapsed (converged/idle) -> fresh count, clears any stale block
|
|
524
|
+
_rr["last_ts"] = _now
|
|
525
|
+
if _rr["count"] >= _RECYREC_MAX:
|
|
526
|
+
_rr["blocked_ts"] = _now
|
|
527
|
+
_rr["reason"] = "recycle_no_converge"
|
|
528
|
+
_rrall[_target] = _rr
|
|
529
|
+
_rst["recyrespawn"] = _rrall
|
|
530
|
+
_save_state(_rst)
|
|
531
|
+
_log(f"RECYCLE-STUCK {_target}: {_rr['count']} recycle-respawns in <={_RECYREC_WINDOW_S}s "
|
|
532
|
+
f"without converging — spawn env likely older than hostd; align it "
|
|
533
|
+
f"(e.g. ~/meshcode-env/bin/pip install -U meshcode==<disk>). BLOCKING recycle-respawns "
|
|
534
|
+
f"for {_RECYREC_BLOCK_TTL_S}s. [recycle_blocked_reason=recycle_no_converge]")
|
|
535
|
+
try: # stop the dashboard's eternal 'launching…' spinner (same pattern as the breaker)
|
|
536
|
+
_rpc("mc_resolve_launch", {
|
|
537
|
+
"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent": agent,
|
|
538
|
+
"p_status": "failed", "p_reason": "recycle_no_converge",
|
|
539
|
+
"p_detail": "recycle keeps relaunching without converging — this agent's MCP env is "
|
|
540
|
+
"older than the host; align it (pip install -U meshcode==<host version>), "
|
|
541
|
+
"then Start again"})
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|
|
544
|
+
continue
|
|
545
|
+
_rrall[_target] = _rr
|
|
546
|
+
_rst["recyrespawn"] = _rrall
|
|
547
|
+
_save_state(_rst)
|
|
506
548
|
_ok, _burst, _why = _spawn_rate_ok(_target)
|
|
507
549
|
if not _ok:
|
|
508
550
|
_log(f"SKIP {'recycle-' if _is_recycle else ''}respawn {_target}: rate-limited ({_why})")
|
|
@@ -519,11 +561,17 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
519
561
|
except Exception:
|
|
520
562
|
pass
|
|
521
563
|
continue
|
|
564
|
+
# Part 2 (Samuel req #2): for a VISIBLE recycle, snapshot the OLD window
|
|
565
|
+
# pid(s) BEFORE spawning the fresh one — so the new pid is never in the
|
|
566
|
+
# close set (commander q1: never touch the fresh terminal).
|
|
567
|
+
_old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
|
|
522
568
|
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
523
569
|
f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
524
570
|
if _spawn_agent(proj, agent, headless=_hl):
|
|
525
571
|
_record_spawn(_target) # count the terminal we just opened, against the breaker
|
|
526
572
|
if _is_recycle:
|
|
573
|
+
if _visible and _old_vis_pids:
|
|
574
|
+
_close_old_visible_recycle(_target, _old_vis_pids) # close old window (DRY-RUN first)
|
|
527
575
|
_rpc("mc_record_recycle",
|
|
528
576
|
{"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
|
|
529
577
|
n += 1
|
|
@@ -591,6 +639,103 @@ def _save_state(st: dict) -> None:
|
|
|
591
639
|
pass
|
|
592
640
|
|
|
593
641
|
|
|
642
|
+
# ------------------------------------------------------------------
|
|
643
|
+
# Recycle non-convergence guard (451d33a0). The env-mismatch storm is a recycle-
|
|
644
|
+
# RESPAWN loop (log: "RECYCLE-RESPAWN <agent> (stale 15-37s, count=0)" every
|
|
645
|
+
# ~15-37s) that the version-recycle guard in _do_version_recycles can NOT see —
|
|
646
|
+
# that guard sits on the version-recycle REQUEST path and its counter only
|
|
647
|
+
# advances on a successful mc_request_recycle, so when a recycle is already
|
|
648
|
+
# pending (requested=False) the counter stays 0 and never trips. This guard sits
|
|
649
|
+
# on the recycle fast-path itself: if a target recycle-respawns _RECYREC_MAX times
|
|
650
|
+
# within _RECYREC_WINDOW_S without converging, STOP relaunching it + alert once
|
|
651
|
+
# (env mismatch) instead of storming terminals forever. Self-heals — the block
|
|
652
|
+
# lifts after _RECYREC_BLOCK_TTL_S (one clean retry; re-blocks if it storms again),
|
|
653
|
+
# and the counter resets the moment the target stops re-qualifying (converged).
|
|
654
|
+
_RECYREC_MAX = _env_int("MESHCODE_RECYREC_MAX", 3, 2) # recycle-respawns before non-converge
|
|
655
|
+
_RECYREC_WINDOW_S = _env_int("MESHCODE_RECYREC_WINDOW_SEC", 120, 30) # consecutive-respawn counting window
|
|
656
|
+
_RECYREC_BLOCK_TTL_S = _env_int("MESHCODE_RECYREC_BLOCK_TTL_SEC", 600, 60) # block duration before a retry
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _recycle_blocked(st, target, now=None):
|
|
660
|
+
"""Shared guard consulted by BOTH _do_respawns (skip the recycle fast-path)
|
|
661
|
+
and _do_version_recycles (don't version-recycle a non-converging target).
|
|
662
|
+
Returns a reason string while `target` is blocked, else None. The block
|
|
663
|
+
auto-expires after _RECYREC_BLOCK_TTL_S so a fixed env gets one clean retry."""
|
|
664
|
+
now = now if now is not None else time.time()
|
|
665
|
+
rec = (st.get("recyrespawn") or {}).get(target) or {}
|
|
666
|
+
bts = rec.get("blocked_ts")
|
|
667
|
+
if bts:
|
|
668
|
+
try:
|
|
669
|
+
if (now - float(bts)) < _RECYREC_BLOCK_TTL_S:
|
|
670
|
+
return rec.get("reason") or "recycle_no_converge"
|
|
671
|
+
except (TypeError, ValueError):
|
|
672
|
+
return None
|
|
673
|
+
return None
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# Samuel rule 2026-06-04: never recycle a CONNECTED agent (live MCP session) except
|
|
677
|
+
# the >3h uptime lifecycle. BUSY_STATUSES (working/online/busy) MISSES a connected-
|
|
678
|
+
# but-idle agent (status idle/standby, window open, heartbeat fresh) — a fresh
|
|
679
|
+
# heartbeat is the stronger 'live session' signal, so an idle-but-connected agent
|
|
680
|
+
# was being version-recycled out from under the user (the storm he kept seeing).
|
|
681
|
+
_CONNECTED_HEARTBEAT_S = _env_int("MESHCODE_CONNECTED_HEARTBEAT_SEC", 60, 10)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _agent_connected(a) -> bool:
|
|
685
|
+
"""True if the agent has a live MCP session: an explicitly-busy status OR a
|
|
686
|
+
very-recent heartbeat (window open even when idle/standby)."""
|
|
687
|
+
if (a.get("status") or "") in BUSY_STATUSES:
|
|
688
|
+
return True
|
|
689
|
+
hb = a.get("heartbeat_age_s")
|
|
690
|
+
try:
|
|
691
|
+
return hb is not None and float(hb) < _CONNECTED_HEARTBEAT_S
|
|
692
|
+
except (TypeError, ValueError):
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# Part 2 (Samuel req #2 2026-06-04): on a VISIBLE recycle, close the OLD window so
|
|
697
|
+
# old+new don't both stay open (audit gap 6a203baa). DRY-RUN first (commander q2 +
|
|
698
|
+
# reaper safe-arm pattern): log-only until the logs confirm it's ONLY the old pid.
|
|
699
|
+
_CLOSE_OLD_VISIBLE_DRYRUN = True
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _pid_alive(pid) -> bool:
|
|
703
|
+
if not pid:
|
|
704
|
+
return False
|
|
705
|
+
try:
|
|
706
|
+
os.kill(int(pid), 0)
|
|
707
|
+
return True
|
|
708
|
+
except ProcessLookupError:
|
|
709
|
+
return False
|
|
710
|
+
except PermissionError:
|
|
711
|
+
return True # exists, owned by another uid — treat as alive (don't guess)
|
|
712
|
+
except Exception:
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _close_old_visible_recycle(target: str, old_pids) -> int:
|
|
717
|
+
"""Close the OLD window's still-alive process on a VISIBLE recycle. `old_pids`
|
|
718
|
+
is the PRE-SPAWN snapshot, so the freshly-opened window's pid is excluded by
|
|
719
|
+
construction — we NEVER touch the fresh terminal (commander q1). Graceful
|
|
720
|
+
self-exit (the stop-hook ends the session on must_exit=recycle) is PRIMARY
|
|
721
|
+
(q3): an already-exited old pid is skipped. DRY-RUN first (q2): log the
|
|
722
|
+
would-close pid; flip _CLOSE_OLD_VISIBLE_DRYRUN=False to arm once logs show
|
|
723
|
+
it's only the old pid. Real kill reuses _kill_headless_pid's cmdline guard."""
|
|
724
|
+
n = 0
|
|
725
|
+
for pid in old_pids:
|
|
726
|
+
if not _pid_alive(pid):
|
|
727
|
+
continue # already self-closed gracefully (q3 primary) — nothing to do
|
|
728
|
+
if _CLOSE_OLD_VISIBLE_DRYRUN:
|
|
729
|
+
_log(f"CLOSE-OLD-VISIBLE-DRYRUN {target}: WOULD close old window pid {pid} "
|
|
730
|
+
f"(visible recycle; fresh window already spawned + excluded) — log-only. "
|
|
731
|
+
f"Flip _CLOSE_OLD_VISIBLE_DRYRUN=False after confirming it's ONLY the old pid.")
|
|
732
|
+
continue
|
|
733
|
+
if _kill_headless_pid(target, pid):
|
|
734
|
+
_log(f"CLOSE-OLD-VISIBLE {target}: closed old window pid {pid} (visible recycle; kept fresh window)")
|
|
735
|
+
n += 1
|
|
736
|
+
return n
|
|
737
|
+
|
|
738
|
+
|
|
594
739
|
def _spawn_rate_ok(target: str):
|
|
595
740
|
"""Anti-spam circuit breaker. Returns (ok, tripped_burst, reason).
|
|
596
741
|
|
|
@@ -978,6 +1123,9 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
|
|
|
978
1123
|
agent (headless_pids, with _kill_headless_pid's reuse-guard) — NEVER blind cmdline. After the
|
|
979
1124
|
kill it goes stale and the recycle FAST-PATH in _do_respawns relaunches it within seconds ->
|
|
980
1125
|
SessionStart restores the handoff. Returns number force-killed."""
|
|
1126
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op
|
|
1127
|
+
# (no recycles are triggered, so there is nothing to enforce). Crash-RESPAWN unaffected.
|
|
1128
|
+
return 0
|
|
981
1129
|
res = _rpc("mc_recycle_enforce_candidates", {"p_api_key": api_key, "p_host_id": host_id})
|
|
982
1130
|
if not res or not res.get("ok"):
|
|
983
1131
|
return 0
|
|
@@ -1023,6 +1171,11 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
|
|
|
1023
1171
|
|
|
1024
1172
|
def _do_recycles(api_key: str, host_id: str) -> int:
|
|
1025
1173
|
"""Uptime-based recycle at task boundary. Returns number recycled."""
|
|
1174
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): the RECYCLE feature is disabled in
|
|
1175
|
+
# prod (unreliable — kept causing version/env-mismatch storms). Hard no-op in source so
|
|
1176
|
+
# it stays dead even if a schedule row reappears. Crash-RESPAWN (_do_respawns) is
|
|
1177
|
+
# UNAFFECTED — only RECYCLE triggers are killed.
|
|
1178
|
+
return 0
|
|
1026
1179
|
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
1027
1180
|
if not cfg or not cfg.get("ok"):
|
|
1028
1181
|
return 0
|
|
@@ -1413,6 +1566,10 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
|
|
|
1413
1566
|
mid-task), rate-limited (<=1 version-recycle per agent / 30min), recycle-not-kill (clean handoff via
|
|
1414
1567
|
mc_request_recycle -> agent exits at its boundary -> _do_respawns relaunches on the new version).
|
|
1415
1568
|
Recorded via mc_record_recycle (NEVER counts against the mig406 crash respawn cap). Owner-scoped."""
|
|
1569
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op.
|
|
1570
|
+
# This was the env-mismatch storm source; fixed-at-source via run_agent env-sync, but the
|
|
1571
|
+
# whole recycle feature is being removed per owner. Crash-RESPAWN is unaffected.
|
|
1572
|
+
return 0
|
|
1416
1573
|
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
1417
1574
|
if not cfg or not cfg.get("ok"):
|
|
1418
1575
|
return 0
|
|
@@ -1437,11 +1594,15 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
|
|
|
1437
1594
|
continue # agent already on the on-disk version (or newer) — nothing to do
|
|
1438
1595
|
except Exception:
|
|
1439
1596
|
continue
|
|
1440
|
-
if (a
|
|
1441
|
-
continue #
|
|
1597
|
+
if _agent_connected(a):
|
|
1598
|
+
continue # Samuel rule: never version-recycle a CONNECTED agent (live MCP session,
|
|
1599
|
+
# even if idle/standby) — the >3h uptime lifecycle (_do_recycles) is the
|
|
1600
|
+
# only recycle that may touch a connected agent.
|
|
1442
1601
|
proj, agent = a.get("project_name"), a.get("name")
|
|
1443
1602
|
if not proj or not agent:
|
|
1444
1603
|
continue
|
|
1604
|
+
if _recycle_blocked(st, f"{proj}/{agent}", now):
|
|
1605
|
+
continue # 451d33a0 unify: already blocked as a non-converging recycle — don't also version-recycle it
|
|
1445
1606
|
key = f"verrec/{a.get('project_id')}/{agent}"
|
|
1446
1607
|
if now - float(st.get(key, 0) or 0) < 1800:
|
|
1447
1608
|
continue # rate-limit: <=1 version-recycle per agent / 30min (no recycle-storm)
|
|
@@ -1731,6 +1731,8 @@ def _build_instructions() -> str:
|
|
|
1731
1731
|
|
|
1732
1732
|
RULES: MCP tools only. Tasks > messages. Messages <100 tokens (long → task). No empty acks, JSON only. Thread via in_reply_to. sensitive=True for secrets. Sync vs async: for turn-based/shared-state work (chess, lock-step, "who goes first"), use meshcode_call (sync RPC, 30s) — async meshcode_send crosses ~50%/turn between same-model agents and yields-pongs.
|
|
1733
1733
|
|
|
1734
|
+
REPLY TO HUMANS VIA THE MESH, NOT THE TERMINAL (product default, Samuel 2026-06-04): when you answer a human (sammybenu/Samuel/ian/fis), you MUST meshcode_send to the mesh. The human watches the DASHBOARD — your Claude Code terminal text and your thinking are INVISIBLE to them, so a terminal-only reply reads as silence ("¿por qué no me contestas?" even while you "answered"). meshcode_send is the ONLY channel they see. To humans write plain prose in THEIR language (Spanish for Samuel), never JSON; JSON-only is for agent↔agent.
|
|
1735
|
+
|
|
1734
1736
|
CLOSE TASKS IMMEDIATELY (#2 rule, sammybenu 2026-05-22T21:06Z): every time you finish/ship/deliver work for a claimed task, call meshcode_task_complete BEFORE re-entering wait. NO "I'll close it after one more thing." NO leaving claimed tasks lingering in_progress while you do something else — that creates phantom in-progress on the dashboard and the human gets angry. If the work needs human review, set requires_approval=true at task_create OR call meshcode_task_complete (it routes to in_review if reviewer set). The mesh dashboard reflects mc_tasks.status in real-time; stale in_progress = lying to the human.
|
|
1735
1737
|
|
|
1736
1738
|
WORK ASSIGNED TASKS IMMEDIATELY (#3 rule): when meshcode_wait returns pending_tasks or auto_started_task, your NEXT action MUST be to work that task. Do NOT re-enter wait, do NOT ask "what should I do" — the task description tells you. Sequence: read task description → execute the work → meshcode_task_complete → meshcode_wait (which surfaces the next task). One by one until the queue is empty.
|
|
@@ -6162,12 +6164,10 @@ def meshcode_recycle_agent(name: str, visible: bool = False) -> Dict[str, Any]:
|
|
|
6162
6164
|
visible: True = respawn as a visible focused window; default False =
|
|
6163
6165
|
preserve the agent's current headless/visible state.
|
|
6164
6166
|
"""
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
"p_visible": bool(visible),
|
|
6170
|
-
})
|
|
6167
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod
|
|
6168
|
+
# (unreliable). No-op — do not call the RPC.
|
|
6169
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6170
|
+
"error": "recycle is disabled (feature removed — was unreliable). No action taken."}
|
|
6171
6171
|
|
|
6172
6172
|
|
|
6173
6173
|
@mcp.tool()
|
|
@@ -6185,12 +6185,9 @@ def meshcode_recycle_fleet(visible: bool = False) -> Dict[str, Any]:
|
|
|
6185
6185
|
visible: True = respawn each as a visible focused window; default False
|
|
6186
6186
|
= preserve each agent's current headless/visible state.
|
|
6187
6187
|
"""
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
"p_agent": None, # NULL = whole fleet (all running agents)
|
|
6192
|
-
"p_visible": bool(visible),
|
|
6193
|
-
})
|
|
6188
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod. No-op.
|
|
6189
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6190
|
+
"error": "recycle is disabled (feature removed — was unreliable). No action taken."}
|
|
6194
6191
|
|
|
6195
6192
|
|
|
6196
6193
|
@mcp.tool()
|
|
@@ -6217,12 +6214,10 @@ def meshcode_set_recycle_schedule(interval_hours: int = 6, enabled: bool = True,
|
|
|
6217
6214
|
"error_code": "not_yet_supported",
|
|
6218
6215
|
"error": "per-agent recycle schedule is a fast-follow; pass agent=None for the mesh-global schedule.",
|
|
6219
6216
|
}
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
"p_interval_hours": int(interval_hours),
|
|
6225
|
-
})
|
|
6217
|
+
# DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): auto-recycle scheduling is disabled
|
|
6218
|
+
# in prod. No-op — never (re)enable a schedule.
|
|
6219
|
+
return {"ok": False, "error_code": "recycle_disabled",
|
|
6220
|
+
"error": "auto-recycle scheduling is disabled (feature removed — was unreliable). No action taken."}
|
|
6226
6221
|
|
|
6227
6222
|
|
|
6228
6223
|
@mcp.tool()
|
|
@@ -17,15 +17,125 @@ from __future__ import annotations
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
19
|
import platform
|
|
20
|
+
import re
|
|
20
21
|
import shlex
|
|
21
22
|
import shutil
|
|
22
23
|
import subprocess
|
|
23
24
|
import sys
|
|
25
|
+
import time
|
|
24
26
|
import urllib.parse
|
|
25
27
|
from pathlib import Path
|
|
26
28
|
from typing import Iterable, Optional
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
# SECURITY (audit P1-4 RCE/DoS): agent names get interpolated into the platform
|
|
32
|
+
# shell / cmd.exe launch string below. shlex.quote is POSIX-only and does NOT
|
|
33
|
+
# neutralize cmd.exe metacharacters, so a name like `x" & calc & "` would break
|
|
34
|
+
# out of the quoting on Windows (cmd /k) and execute arbitrary code. Real agent
|
|
35
|
+
# names are always plain identifiers — hard-reject anything else at the boundary.
|
|
36
|
+
# Hardened allowlist (chief@mesh-dev spec):
|
|
37
|
+
# - first char [A-Za-z0-9_] -> NO leading dash, so `-rf` / `--version` can't be
|
|
38
|
+
# read as a flag/arg if the name ever reaches a bare positional (arg-injection)
|
|
39
|
+
# - then [A-Za-z0-9_-]{0,63} -> total length 1..64, capping the 300-char-name DoS
|
|
40
|
+
# - anchor with \Z, not $ -> in Python `$` also matches just before a trailing
|
|
41
|
+
# newline, so `name\n` would slip through `^...$`. \Z = absolute end of string.
|
|
42
|
+
_VALID_AGENT_NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}\Z")
|
|
43
|
+
|
|
44
|
+
# Launch-storm control (audit P1: each "Launch All" click spawns a terminal per
|
|
45
|
+
# agent UNCONDITIONALLY; repeat clicks storm the desktop with dup terminals).
|
|
46
|
+
# Two dedup layers + a hard cap:
|
|
47
|
+
# (1) heartbeat liveness — skip agents already running (best-effort, network)
|
|
48
|
+
# (2) local cooldown file — per-agent last-spawn ts; covers the spawn->first-
|
|
49
|
+
# heartbeat gap where (1) is blind, and absorbs rapid repeat-clicks. No
|
|
50
|
+
# network, so it is the always-on backstop.
|
|
51
|
+
_MAX_BATCH = 32 # hard cap: agents spawned / call
|
|
52
|
+
_LAUNCH_COOLDOWN_S = 30 # min seconds between same-agent spawns
|
|
53
|
+
_LIVE_HEARTBEAT_S = 20 # heartbeat age < this = agent live
|
|
54
|
+
_COOLDOWN_FILE = Path.home() / ".meshcode" / "launch_cooldown.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_valid_agent_name(name: str) -> bool:
|
|
58
|
+
"""Shared allowlist gate — True iff `name` is a safe agent identifier.
|
|
59
|
+
|
|
60
|
+
Single source of truth for the launch path AND `meshcode run` (run_agent),
|
|
61
|
+
so the RCE/DoS boundary can't drift between callers.
|
|
62
|
+
"""
|
|
63
|
+
return bool(name) and bool(_VALID_AGENT_NAME.match(name))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_cooldowns() -> dict:
|
|
67
|
+
try:
|
|
68
|
+
return json.loads(_COOLDOWN_FILE.read_text(encoding="utf-8")) or {}
|
|
69
|
+
except Exception:
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _record_spawn(name: str, now: Optional[float] = None) -> None:
|
|
74
|
+
"""Stamp `name`'s last-spawn ts in the cooldown file (best-effort, atomic)."""
|
|
75
|
+
try:
|
|
76
|
+
data = _read_cooldowns()
|
|
77
|
+
data[name] = now if now is not None else time.time()
|
|
78
|
+
_COOLDOWN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
tmp = _COOLDOWN_FILE.with_suffix(".json.tmp")
|
|
80
|
+
tmp.write_text(json.dumps(data), encoding="utf-8")
|
|
81
|
+
tmp.replace(_COOLDOWN_FILE)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _in_cooldown(name: str, cooldowns: dict, now: float) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
return (now - float(cooldowns.get(name, 0))) < _LAUNCH_COOLDOWN_S
|
|
89
|
+
except (TypeError, ValueError):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def live_agent_names(names: Iterable[str], project: Optional[str] = None) -> set:
|
|
94
|
+
"""Best-effort set of `names` whose mc_agents heartbeat is fresh (<20s).
|
|
95
|
+
|
|
96
|
+
Shared liveness primitive for the dedup lane (co-owned w/ chief@mesh-dev).
|
|
97
|
+
Network + creds required; ANY failure (offline, no api_key, project
|
|
98
|
+
unresolved, RPC error) returns an EMPTY set so a legitimate launch is never
|
|
99
|
+
blocked — the cooldown file is the always-on backstop. NB: the meshcode://
|
|
100
|
+
launch URL carries no project today, so resolution falls back to
|
|
101
|
+
MESHCODE_PROJECT env; pass `project` explicitly for reliable liveness.
|
|
102
|
+
"""
|
|
103
|
+
wanted = {n for n in names if is_valid_agent_name(n)}
|
|
104
|
+
if not wanted:
|
|
105
|
+
return set()
|
|
106
|
+
try:
|
|
107
|
+
import importlib
|
|
108
|
+
cv = importlib.import_module("meshcode.comms_v4")
|
|
109
|
+
api_key = cv._load_api_key_for_cli()
|
|
110
|
+
proj = project or os.environ.get("MESHCODE_PROJECT")
|
|
111
|
+
if not api_key or not proj:
|
|
112
|
+
return set()
|
|
113
|
+
pid = cv.get_project_id(proj)
|
|
114
|
+
if not pid:
|
|
115
|
+
return set()
|
|
116
|
+
r = cv.sb_rpc("mc_get_agents", {"p_api_key": api_key, "p_project_id": pid,
|
|
117
|
+
"p_agent_name": None, "p_select": None,
|
|
118
|
+
"p_limit": None})
|
|
119
|
+
agents = (r or {}).get("agents") or []
|
|
120
|
+
from datetime import datetime, timezone
|
|
121
|
+
now = datetime.now(timezone.utc)
|
|
122
|
+
live = set()
|
|
123
|
+
for a in agents:
|
|
124
|
+
nm = a.get("name")
|
|
125
|
+
ts = a.get("last_heartbeat")
|
|
126
|
+
if nm not in wanted or not ts:
|
|
127
|
+
continue
|
|
128
|
+
try:
|
|
129
|
+
dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
|
|
130
|
+
if (now - dt).total_seconds() < _LIVE_HEARTBEAT_S:
|
|
131
|
+
live.add(nm)
|
|
132
|
+
except Exception:
|
|
133
|
+
continue
|
|
134
|
+
return live
|
|
135
|
+
except Exception:
|
|
136
|
+
return set()
|
|
137
|
+
|
|
138
|
+
|
|
29
139
|
# ============================================================
|
|
30
140
|
# Per-OS terminal spawn
|
|
31
141
|
# ============================================================
|
|
@@ -165,7 +275,37 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
|
|
|
165
275
|
# Resolve `meshcode` binary path (CLI wrapper installed by pip).
|
|
166
276
|
mc_bin = shutil.which("meshcode") or "meshcode"
|
|
167
277
|
|
|
278
|
+
# RATE LIMIT: hard-cap the batch so a crafted `agents=` list can't spawn an
|
|
279
|
+
# unbounded number of terminals (DoS). Excess is reported, never launched.
|
|
280
|
+
if len(names) > _MAX_BATCH:
|
|
281
|
+
for name in names[_MAX_BATCH:]:
|
|
282
|
+
skipped.append({"agent": name, "reason": f"batch cap {_MAX_BATCH} exceeded"})
|
|
283
|
+
names = names[:_MAX_BATCH]
|
|
284
|
+
|
|
285
|
+
# DEDUP inputs computed ONCE for the whole batch: live set (heartbeat) +
|
|
286
|
+
# cooldown snapshot. `now` is shared so all spawns in this call stamp the
|
|
287
|
+
# same instant.
|
|
288
|
+
live = live_agent_names(names)
|
|
289
|
+
cooldowns = _read_cooldowns()
|
|
290
|
+
now = time.time()
|
|
291
|
+
|
|
168
292
|
for name in names:
|
|
293
|
+
# SECURITY (audit P1-4 RCE/DoS): reject any name that isn't a plain agent
|
|
294
|
+
# identifier BEFORE it reaches the shell/cmd.exe launch string below.
|
|
295
|
+
if not is_valid_agent_name(name):
|
|
296
|
+
skipped.append({"agent": name,
|
|
297
|
+
"reason": "invalid agent name (allowlist ^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$)"})
|
|
298
|
+
continue
|
|
299
|
+
# DEDUP 1: agent already running (fresh heartbeat) -> don't double-spawn.
|
|
300
|
+
if name in live:
|
|
301
|
+
skipped.append({"agent": name, "reason": "already live (heartbeat <20s)"})
|
|
302
|
+
continue
|
|
303
|
+
# DEDUP 2: spawned within the cooldown window -> absorbs repeat-clicks and
|
|
304
|
+
# the spawn->first-heartbeat gap that layer 1 cannot see.
|
|
305
|
+
if _in_cooldown(name, cooldowns, now):
|
|
306
|
+
skipped.append({"agent": name,
|
|
307
|
+
"reason": f"cooldown {_LAUNCH_COOLDOWN_S}s (recently launched)"})
|
|
308
|
+
continue
|
|
169
309
|
# PER-PLATFORM quoting (mesh-core FIX2): cmd.exe wants double-quotes, not POSIX shlex
|
|
170
310
|
# single-quotes (cmd.exe passes single-quotes through literally -> file-not-found).
|
|
171
311
|
if sys.platform == "win32":
|
|
@@ -175,6 +315,7 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
|
|
|
175
315
|
ok, info = _spawn_terminal(cmd)
|
|
176
316
|
if ok:
|
|
177
317
|
launched.append(name)
|
|
318
|
+
_record_spawn(name, now)
|
|
178
319
|
else:
|
|
179
320
|
skipped.append({"agent": name, "reason": info})
|
|
180
321
|
|
|
@@ -695,6 +695,21 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
695
695
|
boot path flips mc_agents.autonomous_mode=true at register time.
|
|
696
696
|
Task f248d98e Phase 1 (no-DDL subset).
|
|
697
697
|
"""
|
|
698
|
+
# SECURITY (audit P1-4): the agent name flows into workspace path lookups and
|
|
699
|
+
# a child editor/`claude` launch. Reject anything that isn't a plain agent
|
|
700
|
+
# identifier (esp. a leading dash, which a child tool could read as a flag) —
|
|
701
|
+
# belt-and-suspenders with the launch-batch allowlist (shared source of truth).
|
|
702
|
+
try:
|
|
703
|
+
from meshcode.protocol_handler import is_valid_agent_name as _ok_name
|
|
704
|
+
except Exception:
|
|
705
|
+
import re as _re
|
|
706
|
+
_ok_name = lambda n: bool(n) and bool(
|
|
707
|
+
_re.match(r"^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}\Z", str(n)))
|
|
708
|
+
if not _ok_name(agent):
|
|
709
|
+
print(json.dumps({"ok": False, "error": f"invalid agent name: {agent!r}",
|
|
710
|
+
"error_code": "invalid_agent_name"}))
|
|
711
|
+
return 1
|
|
712
|
+
|
|
698
713
|
# Propagate autonomous flag to the editor + MCP child via env var.
|
|
699
714
|
if autonomous:
|
|
700
715
|
os.environ["MESHCODE_AUTONOMOUS"] = "1"
|
|
@@ -805,6 +820,22 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
805
820
|
print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
|
|
806
821
|
return 2
|
|
807
822
|
|
|
823
|
+
# Env-mismatch storm fix (2.11.109): the non-blocking auto-pip above updated the
|
|
824
|
+
# LAUNCHER env (sys.executable). But THIS agent's MCP server runs from this
|
|
825
|
+
# workspace's .mcp.json `command` python — that's the env that reports
|
|
826
|
+
# cli_version. Sync IT to the launcher's installed version so hostd
|
|
827
|
+
# version-recycle CONVERGES instead of looping forever. Non-blocking, best-effort.
|
|
828
|
+
if not dry_run:
|
|
829
|
+
try:
|
|
830
|
+
_doc = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
831
|
+
for _srv in (_doc.get("mcpServers") or {}).values():
|
|
832
|
+
_cmd = _srv.get("command")
|
|
833
|
+
if _cmd:
|
|
834
|
+
self_update.sync_agent_env(_cmd)
|
|
835
|
+
break
|
|
836
|
+
except Exception:
|
|
837
|
+
pass
|
|
838
|
+
|
|
808
839
|
# ── Validate stop hook exists (required for wait-loop integrity) ─
|
|
809
840
|
# If the workspace was created on an old CLI that didn't install hooks
|
|
810
841
|
# (or someone wiped .claude/), Claude Code has nothing blocking turn-end
|
|
@@ -275,6 +275,7 @@ state_dir.mkdir(parents=True, exist_ok=True)
|
|
|
275
275
|
mode = sys.argv[1] if len(sys.argv) > 1 else "pip"
|
|
276
276
|
target_version = sys.argv[2] if len(sys.argv) > 2 else None
|
|
277
277
|
site_flag = sys.argv[3] if len(sys.argv) > 3 else "system"
|
|
278
|
+
target_python = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else None # agent MCP-server env
|
|
278
279
|
|
|
279
280
|
try:
|
|
280
281
|
if mode == "pipx":
|
|
@@ -282,11 +283,12 @@ try:
|
|
|
282
283
|
else:
|
|
283
284
|
# --no-cache-dir (task 14782bb4 / urgent): never let pip serve a STALE cached wheel — always
|
|
284
285
|
# fetch the true latest from PyPI (auto-update was grabbing an old cached version otherwise).
|
|
285
|
-
|
|
286
|
+
exe = target_python or sys.executable
|
|
287
|
+
cmd = [exe, "-m", "pip", "install", "-U", "--no-cache-dir",
|
|
286
288
|
"--disable-pip-version-check", "--quiet"]
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
if site_flag == "user":
|
|
289
|
+
# --user only for the LAUNCHER's own user-site install; an explicit
|
|
290
|
+
# target_python (the agent's MCP env / venv) installs into ITS env.
|
|
291
|
+
if site_flag == "user" and not target_python:
|
|
290
292
|
cmd.append("--user")
|
|
291
293
|
cmd.append("meshcode")
|
|
292
294
|
with open(log_path, "ab") as logf:
|
|
@@ -315,12 +317,13 @@ finally:
|
|
|
315
317
|
'''
|
|
316
318
|
|
|
317
319
|
|
|
318
|
-
def _spawn_background_updater(target_version: str) -> bool:
|
|
320
|
+
def _spawn_background_updater(target_version: str, target_python: Optional[str] = None) -> bool:
|
|
319
321
|
"""Spawn a fully detached subprocess that runs the updater.
|
|
320
322
|
|
|
321
323
|
The parent (current `meshcode run`) returns immediately. The child
|
|
322
324
|
runs pip install in the background, writes the result to disk, and
|
|
323
|
-
exits. Next `meshcode run` consumes the result.
|
|
325
|
+
exits. Next `meshcode run` consumes the result. `target_python` (when set)
|
|
326
|
+
is the agent's MCP-server env — pip installs into IT, not the launcher.
|
|
324
327
|
"""
|
|
325
328
|
if not _acquire_lock():
|
|
326
329
|
return False
|
|
@@ -331,7 +334,7 @@ def _spawn_background_updater(target_version: str) -> bool:
|
|
|
331
334
|
# We pass the runner code via stdin so we don't need to ship a
|
|
332
335
|
# second .py file. The child reads it from sys.stdin and execs it.
|
|
333
336
|
runner = f"import sys; exec(sys.stdin.read())"
|
|
334
|
-
args = [sys.executable, "-c", runner, mode, target_version, site_flag]
|
|
337
|
+
args = [sys.executable, "-c", runner, mode, target_version, site_flag, target_python or ""]
|
|
335
338
|
|
|
336
339
|
try:
|
|
337
340
|
if sys.platform == "win32":
|
|
@@ -418,6 +421,46 @@ def check_and_maybe_update(verbose: bool = False) -> None:
|
|
|
418
421
|
print(f"[meshcode] downloading {latest} in background...", file=sys.stderr)
|
|
419
422
|
|
|
420
423
|
|
|
424
|
+
def _env_version(python_exe: str) -> Optional[str]:
|
|
425
|
+
"""meshcode.__version__ as seen by ANOTHER python env (the agent's MCP server)."""
|
|
426
|
+
try:
|
|
427
|
+
out = subprocess.run(
|
|
428
|
+
[python_exe, "-c", "import meshcode,sys; sys.stdout.write(meshcode.__version__)"],
|
|
429
|
+
capture_output=True, text=True, timeout=10).stdout.strip()
|
|
430
|
+
return out or None
|
|
431
|
+
except Exception:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def sync_agent_env(mcp_python: str, verbose: bool = False) -> None:
|
|
436
|
+
"""Bring the agent's MCP-SERVER env (mcp_python, from the workspace .mcp.json
|
|
437
|
+
`command`) up to the LAUNCHER's installed meshcode version.
|
|
438
|
+
|
|
439
|
+
The MCP server is the env that reports cli_version. If it lags hostd's on-disk
|
|
440
|
+
version, hostd version-recycles the agent FOREVER (the env-mismatch storm),
|
|
441
|
+
because run_agent's normal auto-pip only updates the LAUNCHER env
|
|
442
|
+
(sys.executable). This syncs the env that actually matters. Non-blocking
|
|
443
|
+
(background pip). No-op if same env / already current / opted out / unreadable.
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
if not mcp_python:
|
|
447
|
+
return
|
|
448
|
+
if os.path.realpath(mcp_python) == os.path.realpath(sys.executable):
|
|
449
|
+
return # same env — the normal launcher update already covers it
|
|
450
|
+
if update_disabled():
|
|
451
|
+
return
|
|
452
|
+
launcher_ver = _current_version()
|
|
453
|
+
env_ver = _env_version(mcp_python)
|
|
454
|
+
if not launcher_ver or not env_ver:
|
|
455
|
+
return
|
|
456
|
+
if _is_newer(launcher_ver, env_ver):
|
|
457
|
+
if _spawn_background_updater(launcher_ver, mcp_python) and verbose:
|
|
458
|
+
print(f"[meshcode] syncing agent env {env_ver} -> {launcher_ver} in background...",
|
|
459
|
+
file=sys.stderr)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
|
|
421
464
|
# ============================================================
|
|
422
465
|
# Blocking variant — used by `meshcode run` to guarantee the editor
|
|
423
466
|
# subprocess inherits the latest meshcode_mcp/server.py on disk.
|
|
@@ -1333,6 +1333,11 @@ If `meshcode_wait()` times out, call it again with a 2× longer timeout (cap 180
|
|
|
1333
1333
|
- Tasks > messages. Use `meshcode_task_create / task_claim / task_complete`
|
|
1334
1334
|
for trackable work. Keep messages <100 tokens (signals only).
|
|
1335
1335
|
- No empty acks. JSON reports only.
|
|
1336
|
+
- **Reply to humans via the mesh, NOT the terminal** (product default): when you
|
|
1337
|
+
answer a human (Samuel/sammybenu/ian/fis), `meshcode_send` to the mesh. The
|
|
1338
|
+
human watches the DASHBOARD — your Claude Code terminal text + thinking are
|
|
1339
|
+
invisible to them, so a terminal-only reply reads as silence. Write plain prose
|
|
1340
|
+
in their language (Spanish for Samuel), never JSON — JSON is for agent↔agent.
|
|
1336
1341
|
- Threading: pass `in_reply_to`.
|
|
1337
1342
|
- Sync vs async: for turn-based or shared-state coordination (chess, single-writer
|
|
1338
1343
|
doc, lock-step handoffs, "who goes first" decisions), prefer
|