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