polyrob 0.5.0__py3-none-any.whl
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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
cron/schedule.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Schedule parsing + next-run computation for the cron subsystem (roadmap P5).
|
|
2
|
+
|
|
3
|
+
Pure and tz-naive (local wall-clock) so it is deterministic and unit-testable —
|
|
4
|
+
every entry point takes the reference time explicitly. Supported spec formats:
|
|
5
|
+
|
|
6
|
+
- duration interval ............ ``30m``, ``2h``, ``1d``, ``45s`` (also ``every 15m``)
|
|
7
|
+
- one-shot ISO timestamp ....... ``2026-06-07T09:30:00``
|
|
8
|
+
- 5-field cron ................. ``*/15 * * * *`` (min hour dom mon dow; dow 0/7=Sun)
|
|
9
|
+
- every-phrase ................. ``every day 14:30``, ``every monday 09:00``
|
|
10
|
+
|
|
11
|
+
Anything else raises :class:`ScheduleError`.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
_UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
21
|
+
_WEEKDAYS = {
|
|
22
|
+
"monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4,
|
|
23
|
+
"friday": 5, "saturday": 6, "sunday": 0,
|
|
24
|
+
}
|
|
25
|
+
_DURATION_RE = re.compile(r"^(\d+)\s*([smhd])$")
|
|
26
|
+
_HHMM_RE = re.compile(r"^(\d{1,2}):(\d{2})$")
|
|
27
|
+
_MAX_LOOKAHEAD_MINUTES = 366 * 24 * 60 # ~1 year safety bound
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ScheduleError(ValueError):
|
|
31
|
+
"""Raised when a schedule spec cannot be parsed."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Schedule:
|
|
36
|
+
kind: str # "interval" | "cron" | "once"
|
|
37
|
+
one_shot: bool
|
|
38
|
+
interval_seconds: Optional[int] = None
|
|
39
|
+
once_at: Optional[datetime] = None
|
|
40
|
+
cron_fields: Optional[tuple] = None # (minutes, hours, doms, months, dows) sets
|
|
41
|
+
# POSIX day-of-month / day-of-week are OR'd when BOTH are restricted (not "*").
|
|
42
|
+
dom_restricted: bool = False
|
|
43
|
+
dow_restricted: bool = False
|
|
44
|
+
|
|
45
|
+
def next_run_after(self, after: datetime) -> Optional[datetime]:
|
|
46
|
+
"""Return the next fire time strictly after ``after`` (None if none)."""
|
|
47
|
+
if self.kind == "interval":
|
|
48
|
+
return after + timedelta(seconds=self.interval_seconds)
|
|
49
|
+
if self.kind == "once":
|
|
50
|
+
return self.once_at if self.once_at > after else None
|
|
51
|
+
if self.kind == "cron":
|
|
52
|
+
return self._next_cron(after)
|
|
53
|
+
raise ScheduleError(f"unknown schedule kind: {self.kind}")
|
|
54
|
+
|
|
55
|
+
def _next_cron(self, after: datetime) -> Optional[datetime]:
|
|
56
|
+
minutes, hours, doms, months, dows = self.cron_fields
|
|
57
|
+
dt = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
|
|
58
|
+
for _ in range(_MAX_LOOKAHEAD_MINUTES):
|
|
59
|
+
cron_dow = dt.isoweekday() % 7 # Mon..Sun(7)->1..0 ; Sunday == 0
|
|
60
|
+
# POSIX rule: when BOTH day-of-month and day-of-week are restricted, the
|
|
61
|
+
# job runs when EITHER matches (union); otherwise AND the restricted one.
|
|
62
|
+
if self.dom_restricted and self.dow_restricted:
|
|
63
|
+
day_match = (dt.day in doms) or (cron_dow in dows)
|
|
64
|
+
elif self.dom_restricted:
|
|
65
|
+
day_match = dt.day in doms
|
|
66
|
+
elif self.dow_restricted:
|
|
67
|
+
day_match = cron_dow in dows
|
|
68
|
+
else:
|
|
69
|
+
day_match = True
|
|
70
|
+
if (dt.minute in minutes and dt.hour in hours
|
|
71
|
+
and dt.month in months and day_match):
|
|
72
|
+
return dt
|
|
73
|
+
dt += timedelta(minutes=1)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_schedule(spec: str) -> Schedule:
|
|
78
|
+
if not spec or not spec.strip():
|
|
79
|
+
raise ScheduleError("empty schedule spec")
|
|
80
|
+
raw = spec.strip()
|
|
81
|
+
low = raw.lower()
|
|
82
|
+
|
|
83
|
+
# every-phrase
|
|
84
|
+
if low.startswith("every "):
|
|
85
|
+
return _parse_every(low[len("every "):].strip())
|
|
86
|
+
|
|
87
|
+
# duration interval
|
|
88
|
+
m = _DURATION_RE.match(low)
|
|
89
|
+
if m:
|
|
90
|
+
seconds = int(m.group(1)) * _UNIT_SECONDS[m.group(2)]
|
|
91
|
+
if seconds <= 0:
|
|
92
|
+
raise ScheduleError(f"non-positive interval: {spec}")
|
|
93
|
+
return Schedule(kind="interval", one_shot=False, interval_seconds=seconds)
|
|
94
|
+
|
|
95
|
+
# 5-field cron
|
|
96
|
+
if len(raw.split()) == 5:
|
|
97
|
+
return _parse_cron(raw)
|
|
98
|
+
|
|
99
|
+
# ISO timestamp (one-shot)
|
|
100
|
+
try:
|
|
101
|
+
once_at = datetime.fromisoformat(raw)
|
|
102
|
+
except ValueError:
|
|
103
|
+
raise ScheduleError(f"unrecognized schedule spec: {spec!r}")
|
|
104
|
+
# Normalize a tz-aware input to naive LOCAL time so it compares against the
|
|
105
|
+
# scheduler's naive datetime.now() — a tz-aware once_at otherwise raised
|
|
106
|
+
# "can't compare offset-naive and offset-aware datetimes" out of next_run_after.
|
|
107
|
+
if once_at.tzinfo is not None:
|
|
108
|
+
once_at = once_at.astimezone().replace(tzinfo=None)
|
|
109
|
+
return Schedule(kind="once", one_shot=True, once_at=once_at)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _parse_every(rest: str) -> Schedule:
|
|
113
|
+
# "every 15m" -> interval
|
|
114
|
+
m = _DURATION_RE.match(rest)
|
|
115
|
+
if m:
|
|
116
|
+
seconds = int(m.group(1)) * _UNIT_SECONDS[m.group(2)]
|
|
117
|
+
if seconds <= 0:
|
|
118
|
+
# A zero/negative interval makes every scheduler tick see the job as due,
|
|
119
|
+
# re-running it forever (runaway cost). Reject like the bare-duration path.
|
|
120
|
+
raise ScheduleError(f"interval must be positive: every {rest!r}")
|
|
121
|
+
return Schedule(kind="interval", one_shot=False, interval_seconds=seconds)
|
|
122
|
+
|
|
123
|
+
parts = rest.split()
|
|
124
|
+
if len(parts) != 2:
|
|
125
|
+
raise ScheduleError(f"unrecognized 'every' phrase: every {rest!r}")
|
|
126
|
+
when, time_str = parts
|
|
127
|
+
hm = _HHMM_RE.match(time_str)
|
|
128
|
+
if not hm:
|
|
129
|
+
raise ScheduleError(f"bad time in 'every' phrase: {time_str!r}")
|
|
130
|
+
hour, minute = int(hm.group(1)), int(hm.group(2))
|
|
131
|
+
if not (0 <= hour < 24 and 0 <= minute < 60):
|
|
132
|
+
raise ScheduleError(f"time out of range: {time_str!r}")
|
|
133
|
+
|
|
134
|
+
if when == "day":
|
|
135
|
+
dows = set(range(7))
|
|
136
|
+
dow_restricted = False # every day -> DOW not restricted
|
|
137
|
+
elif when in _WEEKDAYS:
|
|
138
|
+
dows = {_WEEKDAYS[when]}
|
|
139
|
+
dow_restricted = True # every <weekday> -> DOW restricted, honor the set
|
|
140
|
+
else:
|
|
141
|
+
raise ScheduleError(f"unknown day in 'every' phrase: {when!r}")
|
|
142
|
+
fields = ({minute}, {hour}, set(range(1, 32)), set(range(1, 13)), dows)
|
|
143
|
+
return Schedule(kind="cron", one_shot=False, cron_fields=fields,
|
|
144
|
+
dom_restricted=False, dow_restricted=dow_restricted)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _parse_cron(raw: str) -> Schedule:
|
|
148
|
+
fields = raw.split()
|
|
149
|
+
ranges = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)]
|
|
150
|
+
try:
|
|
151
|
+
minutes = _expand(fields[0], *ranges[0])
|
|
152
|
+
hours = _expand(fields[1], *ranges[1])
|
|
153
|
+
doms = _expand(fields[2], *ranges[2])
|
|
154
|
+
months = _expand(fields[3], *ranges[3])
|
|
155
|
+
dows = _expand(fields[4], *ranges[4])
|
|
156
|
+
except ScheduleError:
|
|
157
|
+
raise
|
|
158
|
+
except Exception as e:
|
|
159
|
+
raise ScheduleError(f"bad cron expression {raw!r}: {e}")
|
|
160
|
+
if 7 in dows: # cron allows 7 for Sunday; normalize to 0
|
|
161
|
+
dows = (dows - {7}) | {0}
|
|
162
|
+
# A field is "restricted" when it is not a bare/stepped wildcard (Vixie rule:
|
|
163
|
+
# first char != '*'), which selects POSIX OR semantics when both are restricted.
|
|
164
|
+
dom_restricted = not fields[2].strip().startswith("*")
|
|
165
|
+
dow_restricted = not fields[4].strip().startswith("*")
|
|
166
|
+
return Schedule(kind="cron", one_shot=False,
|
|
167
|
+
cron_fields=(minutes, hours, doms, months, dows),
|
|
168
|
+
dom_restricted=dom_restricted, dow_restricted=dow_restricted)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _expand(field: str, lo: int, hi: int) -> set:
|
|
172
|
+
"""Expand one cron field into a set of ints. Supports * , - and */n."""
|
|
173
|
+
result: set = set()
|
|
174
|
+
for part in field.split(","):
|
|
175
|
+
step = 1
|
|
176
|
+
body = part
|
|
177
|
+
if "/" in part:
|
|
178
|
+
body, step_s = part.split("/", 1)
|
|
179
|
+
step = int(step_s)
|
|
180
|
+
if step <= 0:
|
|
181
|
+
raise ScheduleError(f"bad step in {field!r}")
|
|
182
|
+
if body == "*":
|
|
183
|
+
start, end = lo, hi
|
|
184
|
+
elif "-" in body:
|
|
185
|
+
a, b = body.split("-", 1)
|
|
186
|
+
start, end = int(a), int(b)
|
|
187
|
+
else:
|
|
188
|
+
start = end = int(body)
|
|
189
|
+
if start < lo or end > hi or start > end:
|
|
190
|
+
raise ScheduleError(f"cron field out of range: {part!r} (allowed {lo}-{hi})")
|
|
191
|
+
result.update(range(start, end + 1, step))
|
|
192
|
+
if not result:
|
|
193
|
+
raise ScheduleError(f"empty cron field: {field!r}")
|
|
194
|
+
return result
|
cron/scheduler.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Cron tick scheduler (roadmap P5).
|
|
2
|
+
|
|
3
|
+
Finds due jobs, runs each through an injected async ``runner`` under a hard
|
|
4
|
+
duration cap, then reschedules (recurring) or completes (one-shot). A file-based
|
|
5
|
+
tick lock makes a tick safe to fire from multiple processes (``UVICORN_WORKERS>1``)
|
|
6
|
+
without double-running jobs. The runner is injected so the scheduler is fully
|
|
7
|
+
unit-testable without the agent stack; the production runner (see
|
|
8
|
+
``cron.runner``) reuses the agent core with the per-run cap.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Awaitable, Callable, List, Optional
|
|
19
|
+
|
|
20
|
+
from cron.jobs import CronJob, CronJobStore
|
|
21
|
+
from cron.schedule import parse_schedule, ScheduleError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# A held lock older than this (seconds) is considered stale and stolen — guards
|
|
26
|
+
# against a crashed worker leaving the tick permanently locked.
|
|
27
|
+
_LOCK_STALE_SECONDS = 600
|
|
28
|
+
|
|
29
|
+
Runner = Callable[[CronJob], Awaitable[bool]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TickLock:
|
|
33
|
+
"""Exclusive cross-process lock via O_CREAT|O_EXCL, with stale-steal."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, lock_path: str):
|
|
36
|
+
self.lock_path = lock_path
|
|
37
|
+
self._fd: Optional[int] = None
|
|
38
|
+
|
|
39
|
+
def acquire(self) -> bool:
|
|
40
|
+
parent = os.path.dirname(self.lock_path)
|
|
41
|
+
if parent:
|
|
42
|
+
os.makedirs(parent, exist_ok=True)
|
|
43
|
+
try:
|
|
44
|
+
self._fd = os.open(self.lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
45
|
+
except FileExistsError:
|
|
46
|
+
if self._is_stale():
|
|
47
|
+
try:
|
|
48
|
+
os.unlink(self.lock_path)
|
|
49
|
+
except OSError:
|
|
50
|
+
return False
|
|
51
|
+
return self.acquire()
|
|
52
|
+
return False
|
|
53
|
+
os.write(self._fd, str(os.getpid()).encode())
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def _is_stale(self) -> bool:
|
|
57
|
+
try:
|
|
58
|
+
age = time.time() - os.path.getmtime(self.lock_path)
|
|
59
|
+
except OSError:
|
|
60
|
+
return False
|
|
61
|
+
return age > _LOCK_STALE_SECONDS
|
|
62
|
+
|
|
63
|
+
def refresh(self) -> None:
|
|
64
|
+
"""Bump the lock-file mtime so a long-but-LIVE tick is not seen as stale.
|
|
65
|
+
|
|
66
|
+
Without this the mtime is fixed at O_CREAT time; a tick that legitimately
|
|
67
|
+
runs longer than _LOCK_STALE_SECONDS (several due jobs each capped at
|
|
68
|
+
~180s) would be judged stale and stolen by another worker, whose
|
|
69
|
+
reclaim_stale_running() then resets the live worker's rows and double-runs.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
os.utime(self.lock_path, None)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _owner_pid(self) -> Optional[int]:
|
|
77
|
+
try:
|
|
78
|
+
with open(self.lock_path, "rb") as f:
|
|
79
|
+
return int((f.read().strip() or b"0").decode() or "0")
|
|
80
|
+
except (OSError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def release(self) -> None:
|
|
84
|
+
if self._fd is not None:
|
|
85
|
+
try:
|
|
86
|
+
os.close(self._fd)
|
|
87
|
+
finally:
|
|
88
|
+
self._fd = None
|
|
89
|
+
# Ownership-checked unlink: if a heartbeat gap let our lock be stolen and
|
|
90
|
+
# another worker recreated it, do NOT delete THEIR lock.
|
|
91
|
+
try:
|
|
92
|
+
if self._owner_pid() == os.getpid():
|
|
93
|
+
os.unlink(self.lock_path)
|
|
94
|
+
except OSError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class TickResult:
|
|
100
|
+
ran: List[str] = field(default_factory=list)
|
|
101
|
+
failed: List[str] = field(default_factory=list)
|
|
102
|
+
skipped_locked: bool = False
|
|
103
|
+
skipped_busy: bool = False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CronScheduler:
|
|
107
|
+
def __init__(self, store: CronJobStore, runner: Runner, *, lock_path: str):
|
|
108
|
+
self.store = store
|
|
109
|
+
self.runner = runner
|
|
110
|
+
self.lock_path = lock_path
|
|
111
|
+
|
|
112
|
+
async def tick(self, now: Optional[datetime] = None) -> TickResult:
|
|
113
|
+
from core.interactive_gate import is_interactive_busy
|
|
114
|
+
if is_interactive_busy():
|
|
115
|
+
# human mid-turn in the REPL; defer cron execution (jobs stay due for
|
|
116
|
+
# the next idle tick, sharing the CWD workspace safely).
|
|
117
|
+
return TickResult(skipped_busy=True)
|
|
118
|
+
now = now or datetime.now()
|
|
119
|
+
lock = TickLock(self.lock_path)
|
|
120
|
+
if not lock.acquire():
|
|
121
|
+
logger.debug("cron tick skipped: lock held")
|
|
122
|
+
return TickResult(skipped_locked=True)
|
|
123
|
+
try:
|
|
124
|
+
# C2: also hold the cross-process workspace lock so a cron run in this
|
|
125
|
+
# process doesn't mutate the shared CWD workspace while a `rob` REPL in
|
|
126
|
+
# another process is mid-turn. Non-blocking (timeout=0). Acquire is
|
|
127
|
+
# fail-open (any lock error -> defer the tick); errors from the run
|
|
128
|
+
# itself propagate normally (not masked by the lock handling).
|
|
129
|
+
from core.interactive_gate import workspace_turn_lock
|
|
130
|
+
ws = workspace_turn_lock(timeout=0)
|
|
131
|
+
try:
|
|
132
|
+
ws.__enter__()
|
|
133
|
+
except Exception:
|
|
134
|
+
logger.debug("cron tick skipped: workspace lock unavailable/held")
|
|
135
|
+
return TickResult(skipped_busy=True)
|
|
136
|
+
# Heartbeat the tick lock so a legitimately long tick (>_LOCK_STALE_SECONDS)
|
|
137
|
+
# is never seen as stale + stolen by another worker (which would then
|
|
138
|
+
# reclaim + double-run this tick's in-flight jobs).
|
|
139
|
+
hb = asyncio.create_task(self._lock_heartbeat(lock))
|
|
140
|
+
try:
|
|
141
|
+
return await self._run_due(now)
|
|
142
|
+
finally:
|
|
143
|
+
hb.cancel()
|
|
144
|
+
try:
|
|
145
|
+
ws.__exit__(None, None, None)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
finally:
|
|
149
|
+
lock.release()
|
|
150
|
+
|
|
151
|
+
async def _lock_heartbeat(self, lock: "TickLock") -> None:
|
|
152
|
+
interval = max(30.0, _LOCK_STALE_SECONDS / 3.0)
|
|
153
|
+
try:
|
|
154
|
+
while True:
|
|
155
|
+
await asyncio.sleep(interval)
|
|
156
|
+
lock.refresh()
|
|
157
|
+
except asyncio.CancelledError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
async def _run_due(self, now: datetime) -> TickResult:
|
|
161
|
+
result = TickResult()
|
|
162
|
+
# Reclaim crash-orphaned 'running' jobs here — we hold the TickLock, so any
|
|
163
|
+
# 'running' row is genuinely stale (a live run always writes a terminal status
|
|
164
|
+
# after itself). This replaces the unsafe reclaim-in-__init__.
|
|
165
|
+
self.store.reclaim_stale_running()
|
|
166
|
+
for job in self.store.due(now):
|
|
167
|
+
# Atomic claim: only run if WE flipped it scheduled->running. Guards against
|
|
168
|
+
# ever double-running a job (defense-in-depth alongside the TickLock).
|
|
169
|
+
if not self.store.claim_for_run(job.id):
|
|
170
|
+
continue
|
|
171
|
+
success = await self._run_one(job)
|
|
172
|
+
self._record(job, now, success)
|
|
173
|
+
(result.ran if success else result.failed).append(job.id)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
async def _run_one(self, job: CronJob) -> bool:
|
|
177
|
+
try:
|
|
178
|
+
return bool(await asyncio.wait_for(
|
|
179
|
+
self.runner(job), timeout=max(job.max_duration_seconds, 0.001),
|
|
180
|
+
))
|
|
181
|
+
except asyncio.TimeoutError:
|
|
182
|
+
logger.warning("cron job %s timed out after %ss", job.id, job.max_duration_seconds)
|
|
183
|
+
return False
|
|
184
|
+
except Exception as e: # runner blew up — never crash the tick
|
|
185
|
+
logger.error("cron job %s raised: %s", job.id, e, exc_info=True)
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def _record(self, job: CronJob, now: datetime, success: bool) -> None:
|
|
189
|
+
if job.one_shot:
|
|
190
|
+
self.store.update_after_run(
|
|
191
|
+
job.id, last_run_at=now, next_run_at=None,
|
|
192
|
+
status="done" if success else "failed",
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
# recurring: reschedule from the schedule spec
|
|
196
|
+
try:
|
|
197
|
+
nxt = parse_schedule(job.schedule_spec).next_run_after(now)
|
|
198
|
+
except ScheduleError:
|
|
199
|
+
nxt = None
|
|
200
|
+
status = "scheduled" if nxt else "done"
|
|
201
|
+
self.store.update_after_run(job.id, last_run_at=now, next_run_at=nxt, status=status)
|
cron/service.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""CronService — the schedule/list/cancel API over the job store (roadmap P5).
|
|
2
|
+
|
|
3
|
+
Thin, fully testable orchestration of :mod:`cron.schedule` + :mod:`cron.jobs`.
|
|
4
|
+
The agent-facing ``cronjob`` tool (``tools/cronjob_tools.py``) and any HTTP/CLI
|
|
5
|
+
surface delegate here so scheduling logic lives in exactly one place.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from cron.jobs import CronJob, CronJobStore
|
|
14
|
+
from cron.schedule import ScheduleError, parse_schedule
|
|
15
|
+
|
|
16
|
+
_DEFAULT_MAX_DURATION_S = 180 # 3-minute hard cap for cron sessions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CronService:
|
|
20
|
+
def __init__(self, store: CronJobStore, *, now: Optional[Callable[[], datetime]] = None,
|
|
21
|
+
id_factory: Optional[Callable[[], str]] = None):
|
|
22
|
+
self.store = store
|
|
23
|
+
self._now = now or datetime.now
|
|
24
|
+
self._id = id_factory or (lambda: uuid.uuid4().hex[:12])
|
|
25
|
+
|
|
26
|
+
def schedule(self, *, task: str, schedule_spec: str, user_id: str,
|
|
27
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
28
|
+
max_duration_seconds: int = _DEFAULT_MAX_DURATION_S,
|
|
29
|
+
job_id: Optional[str] = None) -> CronJob:
|
|
30
|
+
"""Validate the spec, compute the first run, and persist the job.
|
|
31
|
+
|
|
32
|
+
Raises ScheduleError for an unparseable spec or one with no future run
|
|
33
|
+
(e.g. a one-shot timestamp already in the past).
|
|
34
|
+
|
|
35
|
+
NOTE (ME-D2): ``skip_memory`` was removed from this signature — it was
|
|
36
|
+
never consumed by the runner or the goal dispatcher (dead, inert knob).
|
|
37
|
+
A cron run gets cross-session memory recall like any other session; see
|
|
38
|
+
``CronJob.skip_memory`` in ``cron/jobs.py`` for the dormant DB column.
|
|
39
|
+
"""
|
|
40
|
+
schedule = parse_schedule(schedule_spec) # raises ScheduleError
|
|
41
|
+
now = self._now()
|
|
42
|
+
next_run = schedule.next_run_after(now)
|
|
43
|
+
if next_run is None:
|
|
44
|
+
raise ScheduleError(f"schedule {schedule_spec!r} has no future run time")
|
|
45
|
+
job = CronJob(
|
|
46
|
+
id=job_id or self._id(), task=task, schedule_spec=schedule_spec,
|
|
47
|
+
user_id=user_id, next_run_at=next_run, one_shot=schedule.one_shot,
|
|
48
|
+
max_duration_seconds=max_duration_seconds,
|
|
49
|
+
payload=payload or {}, created_at=now,
|
|
50
|
+
)
|
|
51
|
+
return self.store.add(job)
|
|
52
|
+
|
|
53
|
+
def list_jobs(self, user_id: Optional[str] = None) -> List[CronJob]:
|
|
54
|
+
return self.store.list(user_id=user_id)
|
|
55
|
+
|
|
56
|
+
def cancel(self, job_id: str, *, user_id: Optional[str] = None) -> bool:
|
|
57
|
+
return self.store.cancel(job_id, user_id=user_id)
|
cron/wake_gate.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Wake change-gate: skip a change-gated cron
|
|
2
|
+
review tick when nothing observable changed since the last tick.
|
|
3
|
+
|
|
4
|
+
The observed autonomy economy was ~23/25 no-op review wakes — every tick paid
|
|
5
|
+
for a full model run to conclude "nothing new". The gate computes a cheap
|
|
6
|
+
fingerprint over the tenant's observable autonomy state (goal board + goal
|
|
7
|
+
events + cron activity + newest episode) and compares it to the baseline stored
|
|
8
|
+
at the previous tick; equality means the paid model call is skipped ($0 tick,
|
|
9
|
+
same shape as ``wake_agent=False``).
|
|
10
|
+
|
|
11
|
+
Scope guards (both must hold before a tick can be skipped):
|
|
12
|
+
- global: ``AutonomyConfig.wake_change_gate()`` (``WAKE_CHANGE_GATE``, posture
|
|
13
|
+
``full`` turns it on by default — it pairs with ``CRON_ENABLED``);
|
|
14
|
+
- per-job: ``payload.change_gated`` truthy AND no ``payload.deliver`` — a
|
|
15
|
+
scheduled delivery (daily digest) must fire even with zero change.
|
|
16
|
+
|
|
17
|
+
Fail-open everywhere: a missing DB, a schema surprise, or any exception in a
|
|
18
|
+
fingerprint source contributes a neutral value or disables the skip — the gate
|
|
19
|
+
can only ever suppress a tick when every read succeeded AND matched the
|
|
20
|
+
baseline. The baseline is recorded AFTER a run, tagged with the run's outcome
|
|
21
|
+
(``record_wake_outcome``): a skip requires the last gated run to have SUCCEEDED,
|
|
22
|
+
so a persistently-failing job (which mutates no observable state and would
|
|
23
|
+
otherwise fingerprint as "unchanged") always retries instead of being silently
|
|
24
|
+
swallowed.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import sqlite3
|
|
32
|
+
import time
|
|
33
|
+
from typing import Any, Callable, Optional
|
|
34
|
+
|
|
35
|
+
from core.sqlite_util import execute_retry
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _neutral_on_missing_table(fn: Callable[[], str], neutral: str) -> str:
|
|
41
|
+
"""A missing table/DB is a known-empty source (nothing to observe) → a stable
|
|
42
|
+
neutral token. Any OTHER error re-raises so the caller's fail-open varying
|
|
43
|
+
token forces the tick to run."""
|
|
44
|
+
try:
|
|
45
|
+
return fn()
|
|
46
|
+
except sqlite3.OperationalError as e:
|
|
47
|
+
if "no such table" in str(e).lower():
|
|
48
|
+
return neutral
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WakeGateStore:
|
|
53
|
+
"""Per-job fingerprint baselines, colocated in cron.db (the gate is a cron
|
|
54
|
+
concern; a side table keeps the baseline server-owned and out of the
|
|
55
|
+
agent-mutable job payload)."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, db_path: str):
|
|
58
|
+
self.db_path = db_path
|
|
59
|
+
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
|
|
60
|
+
execute_retry(
|
|
61
|
+
db_path,
|
|
62
|
+
"""CREATE TABLE IF NOT EXISTS wake_gate (
|
|
63
|
+
job_id TEXT PRIMARY KEY,
|
|
64
|
+
user_id TEXT,
|
|
65
|
+
fingerprint TEXT NOT NULL,
|
|
66
|
+
ok INTEGER NOT NULL DEFAULT 1,
|
|
67
|
+
updated_at REAL NOT NULL
|
|
68
|
+
)""",
|
|
69
|
+
)
|
|
70
|
+
try: # idempotent migration for pre-`ok` tables
|
|
71
|
+
execute_retry(db_path, "ALTER TABLE wake_gate ADD COLUMN ok INTEGER NOT NULL DEFAULT 1")
|
|
72
|
+
except sqlite3.OperationalError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def get(self, job_id: str) -> Optional[dict]:
|
|
76
|
+
row = execute_retry(
|
|
77
|
+
self.db_path, "SELECT fingerprint, ok FROM wake_gate WHERE job_id=?",
|
|
78
|
+
(job_id,), fetch="one",
|
|
79
|
+
)
|
|
80
|
+
return dict(row) if row else None
|
|
81
|
+
|
|
82
|
+
def put(self, job_id: str, user_id: str, fingerprint: str, *, ok: bool = True) -> None:
|
|
83
|
+
execute_retry(
|
|
84
|
+
self.db_path,
|
|
85
|
+
"""INSERT INTO wake_gate (job_id, user_id, fingerprint, ok, updated_at)
|
|
86
|
+
VALUES (?,?,?,?,?)
|
|
87
|
+
ON CONFLICT(job_id) DO UPDATE SET
|
|
88
|
+
fingerprint=excluded.fingerprint, ok=excluded.ok,
|
|
89
|
+
updated_at=excluded.updated_at""",
|
|
90
|
+
(job_id, user_id, fingerprint, 1 if ok else 0, time.time()),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _goal_board_cursor(user_id: str, data_dir: str) -> str:
|
|
95
|
+
"""MAX(goal_events.id) for the tenant + goal counts by status — bumps on every
|
|
96
|
+
goal/ask/objective mutation (goal_events is AUTOINCREMENT)."""
|
|
97
|
+
db = os.path.join(data_dir, "goals.db")
|
|
98
|
+
if not os.path.exists(db):
|
|
99
|
+
return "goals:none"
|
|
100
|
+
return _neutral_on_missing_table(lambda: _goal_board_read(user_id, db), "goals:none")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _goal_board_read(user_id: str, db: str) -> str:
|
|
104
|
+
max_ev = execute_retry(
|
|
105
|
+
db,
|
|
106
|
+
"SELECT MAX(e.id) AS m FROM goal_events e JOIN goals g ON g.id=e.goal_id "
|
|
107
|
+
"WHERE g.user_id=?",
|
|
108
|
+
(user_id,), fetch="one",
|
|
109
|
+
)
|
|
110
|
+
counts = execute_retry(
|
|
111
|
+
db,
|
|
112
|
+
"SELECT status, COUNT(*) AS n FROM goals WHERE user_id=? GROUP BY status "
|
|
113
|
+
"ORDER BY status",
|
|
114
|
+
(user_id,), fetch="all",
|
|
115
|
+
)
|
|
116
|
+
count_sig = ",".join(f"{r['status']}={r['n']}" for r in (counts or []))
|
|
117
|
+
return f"goals:{(max_ev or {})['m'] if max_ev else None}:{count_sig}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cron_cursor(user_id: str, data_dir: str, exclude_job_id: Optional[str]) -> str:
|
|
121
|
+
"""Newest last_run_at across the tenant's OTHER cron jobs (another job having
|
|
122
|
+
run is observable change for a review tick)."""
|
|
123
|
+
db = os.path.join(data_dir, "cron.db")
|
|
124
|
+
if not os.path.exists(db):
|
|
125
|
+
return "cron:none"
|
|
126
|
+
|
|
127
|
+
def read() -> str:
|
|
128
|
+
row = execute_retry(
|
|
129
|
+
db,
|
|
130
|
+
"SELECT MAX(last_run_at) AS m FROM cron_jobs WHERE user_id=? AND id != ?",
|
|
131
|
+
(user_id, exclude_job_id or ""), fetch="one",
|
|
132
|
+
)
|
|
133
|
+
return f"cron:{row['m'] if row else None}"
|
|
134
|
+
|
|
135
|
+
return _neutral_on_missing_table(read, "cron:none")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _episode_cursor(user_id: str, data_dir: str) -> str:
|
|
139
|
+
"""Newest episode row for the tenant (a completed session/goal/cron run since
|
|
140
|
+
the last tick is observable change). Reads the memory DB directly and
|
|
141
|
+
fail-opens to neutral on any surprise — the provider owns the schema."""
|
|
142
|
+
for name in ("memory.db",):
|
|
143
|
+
db = os.path.join(data_dir, name)
|
|
144
|
+
if not os.path.exists(db):
|
|
145
|
+
continue
|
|
146
|
+
def read() -> str:
|
|
147
|
+
row = execute_retry(
|
|
148
|
+
db, "SELECT MAX(id) AS m FROM episodes WHERE user_id=?",
|
|
149
|
+
(user_id,), fetch="one",
|
|
150
|
+
)
|
|
151
|
+
return f"episodes:{row['m'] if row else None}"
|
|
152
|
+
|
|
153
|
+
return _neutral_on_missing_table(read, "episodes:none")
|
|
154
|
+
return "episodes:none"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def compute_wake_fingerprint(
|
|
158
|
+
user_id: str, *, data_dir: str, exclude_job_id: Optional[str] = None
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Stable hash of the tenant's observable autonomy state (cheap, no LLM)."""
|
|
161
|
+
parts = []
|
|
162
|
+
for source in (
|
|
163
|
+
lambda: _goal_board_cursor(user_id, data_dir),
|
|
164
|
+
lambda: _cron_cursor(user_id, data_dir, exclude_job_id),
|
|
165
|
+
lambda: _episode_cursor(user_id, data_dir),
|
|
166
|
+
):
|
|
167
|
+
try:
|
|
168
|
+
parts.append(source())
|
|
169
|
+
except Exception:
|
|
170
|
+
# a failing source contributes a per-call-varying token so the
|
|
171
|
+
# fingerprint reads as "changed" — fail-open, never suppress a wake
|
|
172
|
+
parts.append(f"err:{time.time()}")
|
|
173
|
+
return hashlib.sha256("|".join(parts).encode()).hexdigest()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def gate_applies(job: Any) -> bool:
|
|
177
|
+
"""Both guards: the global flag AND the per-job opt-in (never delivery jobs)."""
|
|
178
|
+
try:
|
|
179
|
+
from agents.task.constants import AutonomyConfig
|
|
180
|
+
if not AutonomyConfig.wake_change_gate():
|
|
181
|
+
return False
|
|
182
|
+
payload = dict(getattr(job, "payload", None) or {})
|
|
183
|
+
return bool(payload.get("change_gated")) and not payload.get("deliver")
|
|
184
|
+
except Exception:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def should_skip_wake(job: Any, *, data_dir: str) -> bool:
|
|
189
|
+
"""Whether this change-gated tick can be skipped ($0). READ-ONLY: a skip
|
|
190
|
+
requires an existing baseline that (a) matches the current fingerprint and
|
|
191
|
+
(b) was recorded after a SUCCESSFUL run — a failed run never establishes a
|
|
192
|
+
skippable baseline, so failing jobs always retry. Callers record the
|
|
193
|
+
baseline after the run via :func:`record_wake_outcome`."""
|
|
194
|
+
try:
|
|
195
|
+
if not gate_applies(job):
|
|
196
|
+
return False
|
|
197
|
+
store = WakeGateStore(os.path.join(data_dir, "cron.db"))
|
|
198
|
+
baseline = store.get(job.id)
|
|
199
|
+
if not baseline or not baseline.get("ok"):
|
|
200
|
+
return False
|
|
201
|
+
fp = compute_wake_fingerprint(
|
|
202
|
+
job.user_id, data_dir=data_dir, exclude_job_id=getattr(job, "id", None)
|
|
203
|
+
)
|
|
204
|
+
return fp == baseline.get("fingerprint")
|
|
205
|
+
except Exception:
|
|
206
|
+
logger.warning("wake gate error — failing open (running the tick)", exc_info=True)
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def record_wake_outcome(job: Any, *, data_dir: str, ok: bool) -> None:
|
|
211
|
+
"""Record the post-run baseline (fingerprint computed AFTER the run, so the
|
|
212
|
+
run's own writes don't read as 'change' next tick) tagged with the outcome.
|
|
213
|
+
Fail-open — a store error just means the next tick runs."""
|
|
214
|
+
try:
|
|
215
|
+
if not gate_applies(job):
|
|
216
|
+
return
|
|
217
|
+
store = WakeGateStore(os.path.join(data_dir, "cron.db"))
|
|
218
|
+
fp = compute_wake_fingerprint(
|
|
219
|
+
job.user_id, data_dir=data_dir, exclude_job_id=getattr(job, "id", None)
|
|
220
|
+
)
|
|
221
|
+
store.put(job.id, job.user_id, fp, ok=ok)
|
|
222
|
+
except Exception:
|
|
223
|
+
logger.warning("wake gate outcome record failed (next tick will run)", exc_info=True)
|
data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""POLYROB seed data (prompts, etc.) shipped with the package."""
|