meshcode 2.11.94__tar.gz → 2.11.97__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.97}/PKG-INFO +1 -1
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/__init__.py +1 -1
- meshcode-2.11.97/meshcode/daemon.py +492 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/hostd.py +47 -5
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/backend.py +319 -31
- {meshcode-2.11.94/meshcode-tasks-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/realtime.py +5 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/meshcode_mcp/server.py +48 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/self_update.py +203 -4
- meshcode-2.11.97/meshcode/setup_clients.py +1884 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode.egg-info/PKG-INFO +1 -1
- meshcode-2.11.97/meshcode.egg-info/SOURCES.txt +89 -0
- meshcode-2.11.97/meshcode.egg-info/top_level.txt +1 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/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.97}/README.md +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/__main__.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/cli.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/compat.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/doctor.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/invites.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/launcher.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/preferences.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/secrets.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode/up.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/meshcode/upload.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/setup.cfg +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_core.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_doctor.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.94/meshcode-backend-wt → meshcode-2.11.97}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.94 → meshcode-2.11.97}/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:]))
|
|
@@ -403,13 +403,23 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
403
403
|
# on ';' -> file-not-found. Layered ON TOP of the 2.11.72 headless-flags fix (visible branch only).
|
|
404
404
|
# task 14782bb4: CLEAR any inherited NO_*UPDATE (so the agent auto-updates non-blocking) and run
|
|
405
405
|
# via `python -m meshcode` (NOT the .exe shim) so a bg `pip install -U` can replace meshcode.exe.
|
|
406
|
+
# RELIABILITY (commander, REOPEN_PATH_bug): the VISIBLE spawn must also put the meshcode
|
|
407
|
+
# console script on PATH (a8efddeb fixed only the headless branch). Without it the spawned
|
|
408
|
+
# agent + its `meshcode mcp` child die with "meshcode not on PATH". Prepend sys.executable's
|
|
409
|
+
# bin dir, mirroring the headless POSIX fix + the win32 venv-Scripts injection.
|
|
410
|
+
try:
|
|
411
|
+
_bindir = str(Path(sys.executable).resolve().parent)
|
|
412
|
+
except Exception:
|
|
413
|
+
_bindir = ""
|
|
406
414
|
if sys.platform == "win32":
|
|
407
415
|
cmd = (f'set "CLAUDECODE=" & set "CLAUDE_CODE_SESSION=" & '
|
|
408
416
|
f'set "MESHCODE_NO_UPDATE=" & set "MESHCODE_NO_AUTO_UPDATE=" & '
|
|
409
|
-
f'"{
|
|
417
|
+
+ (f'set "PATH={_bindir};%PATH%" & ' if _bindir else '')
|
|
418
|
+
+ f'"{sys.executable}" -m meshcode run "{target}"')
|
|
410
419
|
else:
|
|
411
420
|
cmd = (f"unset CLAUDECODE CLAUDE_CODE_SESSION MESHCODE_NO_UPDATE MESHCODE_NO_AUTO_UPDATE; "
|
|
412
|
-
f"
|
|
421
|
+
+ (f"export PATH={shlex.quote(_bindir)}:$PATH; " if _bindir else "")
|
|
422
|
+
+ f"exec {shlex.quote(sys.executable)} -m meshcode run {shlex.quote(target)}")
|
|
413
423
|
try:
|
|
414
424
|
from meshcode import protocol_handler as _ph
|
|
415
425
|
ok, info = _ph._spawn_terminal(cmd)
|
|
@@ -446,9 +456,15 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
446
456
|
# PROMPTLY (the RPC returned it at a 15s stale gate, not STALE_SECONDS) and recorded as a
|
|
447
457
|
# RECYCLE (mc_record_recycle), NEVER against the crash respawn cap.
|
|
448
458
|
_is_recycle = bool(c.get("recycle_fast"))
|
|
459
|
+
# 798dd1bd: on-demand recycle with recycle_visible -> reopen a FRESH VISIBLE window
|
|
460
|
+
# (Samuel's "terminals reopen fresh"); else PRESERVE the agent's headless state
|
|
461
|
+
# (don't silently un-headless a headless fleet — backend2 A2). One-shot: mc_record_recycle
|
|
462
|
+
# clears recycle_visible so subsequent respawns use the normal headless flag.
|
|
463
|
+
_visible = _is_recycle and bool(c.get("recycle_visible"))
|
|
464
|
+
_hl = False if _visible else bool(c.get("headless"))
|
|
449
465
|
_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=
|
|
466
|
+
f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
467
|
+
if _spawn_agent(proj, agent, headless=_hl):
|
|
452
468
|
if _is_recycle:
|
|
453
469
|
_rpc("mc_record_recycle",
|
|
454
470
|
{"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
|
|
@@ -515,13 +531,38 @@ def _record_headless_pid(target: str, pid: int) -> None:
|
|
|
515
531
|
try:
|
|
516
532
|
st = _load_state()
|
|
517
533
|
pids = st.get("headless_pids") or {}
|
|
518
|
-
pids[target] = pid
|
|
534
|
+
pids[target] = pid # overwrite any stale entry for this target (refresh on respawn)
|
|
519
535
|
st["headless_pids"] = pids
|
|
520
536
|
_save_state(st)
|
|
521
537
|
except Exception:
|
|
522
538
|
pass
|
|
523
539
|
|
|
524
540
|
|
|
541
|
+
def _gc_headless_pids() -> None:
|
|
542
|
+
"""GC dead PIDs from headless_pids (cb90b058) so a stale entry can't mask a live agent +
|
|
543
|
+
the recorded PID stays accurate for enforce. Cheap; runs once per sweep. Best-effort."""
|
|
544
|
+
try:
|
|
545
|
+
st = _load_state()
|
|
546
|
+
pids = st.get("headless_pids") or {}
|
|
547
|
+
if not pids:
|
|
548
|
+
return
|
|
549
|
+
live = {}
|
|
550
|
+
for target, pid in pids.items():
|
|
551
|
+
try:
|
|
552
|
+
os.kill(int(pid), 0) # raises ProcessLookupError if the PID is gone
|
|
553
|
+
live[target] = pid
|
|
554
|
+
except (ProcessLookupError, ValueError):
|
|
555
|
+
pass # dead / bad -> drop
|
|
556
|
+
except Exception:
|
|
557
|
+
live[target] = pid # alive (PermissionError) or unknown -> keep (reuse-guard covers kill-time)
|
|
558
|
+
if len(live) != len(pids):
|
|
559
|
+
st["headless_pids"] = live
|
|
560
|
+
_save_state(st)
|
|
561
|
+
_log(f"GC headless_pids: dropped {len(pids)-len(live)} dead PID(s)")
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
565
|
+
|
|
525
566
|
def _pid_cmdline(pid: int) -> str:
|
|
526
567
|
"""Best-effort command line for a pid (to avoid killing a reused PID). '' on failure."""
|
|
527
568
|
try:
|
|
@@ -1206,6 +1247,7 @@ def cmd_hostd(args: list) -> int:
|
|
|
1206
1247
|
recycled = _do_recycles(api_key, host_id)
|
|
1207
1248
|
ver_recycled = _do_version_recycles(api_key, host_id)
|
|
1208
1249
|
stopped = _do_stops(api_key, host_id)
|
|
1250
|
+
_gc_headless_pids() # cb90b058: drop dead PIDs (stale entry can't mask a live agent)
|
|
1209
1251
|
_up = int(time.monotonic() - _spawn_mono)
|
|
1210
1252
|
if relaunched or recycled or ver_recycled or stopped or enforced:
|
|
1211
1253
|
_log(f"sweep done (uptime={_up}s) — {relaunched} respawned, {recycled} recycled, {ver_recycled} version-recycled, {stopped} stopped, {enforced} recycle-enforced")
|