apsimo 1.3.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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/projects/store.py
ADDED
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
"""ProjectStore -- SQLite persistence for projects + steps (survives restarts)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from apsimo.projects.models import Project, Step
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProjectStore:
|
|
18
|
+
_AUTHORITY_BOUND_PROJECT_SOURCES = frozenset({
|
|
19
|
+
"cognition_spine", "governed_action",
|
|
20
|
+
})
|
|
21
|
+
_AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS = (
|
|
22
|
+
"title", "objective", "source", "concern_id",
|
|
23
|
+
"source_event_refs", "thought_job_id", "thought_result_ref",
|
|
24
|
+
"goal_proposal_id", "evidence_refs", "policy_decision_refs",
|
|
25
|
+
"subject_person_id", "viewer_scope", "shareability",
|
|
26
|
+
"capability_allowlist", "goal_fingerprint", "entity_ids",
|
|
27
|
+
)
|
|
28
|
+
_WORK_ORDER_PROJECT_AUTHORITY_FIELDS = (
|
|
29
|
+
_AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
33
|
+
self._lock = threading.RLock()
|
|
34
|
+
self._conn = sqlite3.connect(str(db_path) if db_path else ":memory:",
|
|
35
|
+
check_same_thread=False)
|
|
36
|
+
self._conn.row_factory = sqlite3.Row
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._conn.execute(
|
|
39
|
+
"""CREATE TABLE IF NOT EXISTS projects (
|
|
40
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, objective TEXT,
|
|
41
|
+
source TEXT, status TEXT, outcome TEXT DEFAULT 'pending',
|
|
42
|
+
entity_ids TEXT, reason TEXT,
|
|
43
|
+
replans INTEGER DEFAULT 0, next_review_at REAL,
|
|
44
|
+
concern_id TEXT, source_event_refs TEXT,
|
|
45
|
+
thought_job_id TEXT, thought_result_ref TEXT,
|
|
46
|
+
goal_proposal_id TEXT, evidence_refs TEXT,
|
|
47
|
+
policy_decision_refs TEXT, subject_person_id TEXT,
|
|
48
|
+
viewer_scope TEXT DEFAULT 'owner',
|
|
49
|
+
shareability TEXT DEFAULT 'owner_private',
|
|
50
|
+
capability_allowlist TEXT, goal_fingerprint TEXT,
|
|
51
|
+
created_at REAL, updated_at REAL
|
|
52
|
+
)""")
|
|
53
|
+
self._conn.execute(
|
|
54
|
+
"""CREATE TABLE IF NOT EXISTS steps (
|
|
55
|
+
id TEXT PRIMARY KEY, project_id TEXT NOT NULL,
|
|
56
|
+
ordinal INTEGER, description TEXT, action_kind TEXT,
|
|
57
|
+
depends_on TEXT, status TEXT, attempts INTEGER DEFAULT 0,
|
|
58
|
+
result TEXT, work_order_ref TEXT, work_order_digest TEXT,
|
|
59
|
+
work_order_issued_at REAL DEFAULT 0, result_ref TEXT,
|
|
60
|
+
boundary_subject TEXT,
|
|
61
|
+
confidence REAL DEFAULT 0.6,
|
|
62
|
+
created_at REAL, updated_at REAL
|
|
63
|
+
)""")
|
|
64
|
+
# Additive migrations only: old databases remain directly
|
|
65
|
+
# readable and rollback can ignore the new columns/tables.
|
|
66
|
+
project_cols = {r[1] for r in self._conn.execute(
|
|
67
|
+
"PRAGMA table_info(projects)").fetchall()}
|
|
68
|
+
for name, definition in (
|
|
69
|
+
("outcome", "TEXT DEFAULT 'pending'"),
|
|
70
|
+
("concern_id", "TEXT"),
|
|
71
|
+
("source_event_refs", "TEXT"),
|
|
72
|
+
("thought_job_id", "TEXT"),
|
|
73
|
+
("thought_result_ref", "TEXT"),
|
|
74
|
+
("goal_proposal_id", "TEXT"),
|
|
75
|
+
("evidence_refs", "TEXT"),
|
|
76
|
+
("policy_decision_refs", "TEXT"),
|
|
77
|
+
("subject_person_id", "TEXT"),
|
|
78
|
+
("viewer_scope", "TEXT DEFAULT 'owner'"),
|
|
79
|
+
("shareability", "TEXT DEFAULT 'owner_private'"),
|
|
80
|
+
("capability_allowlist", "TEXT"),
|
|
81
|
+
("goal_fingerprint", "TEXT"),
|
|
82
|
+
):
|
|
83
|
+
if name not in project_cols:
|
|
84
|
+
self._conn.execute(
|
|
85
|
+
f"ALTER TABLE projects ADD COLUMN {name} {definition}")
|
|
86
|
+
cols = {r[1] for r in self._conn.execute(
|
|
87
|
+
"PRAGMA table_info(steps)").fetchall()}
|
|
88
|
+
for name, definition in (
|
|
89
|
+
("confidence", "REAL DEFAULT 0.6"),
|
|
90
|
+
("work_order_ref", "TEXT"),
|
|
91
|
+
("work_order_digest", "TEXT"),
|
|
92
|
+
("work_order_issued_at", "REAL DEFAULT 0"),
|
|
93
|
+
("result_ref", "TEXT"),
|
|
94
|
+
):
|
|
95
|
+
if name not in cols:
|
|
96
|
+
self._conn.execute(
|
|
97
|
+
f"ALTER TABLE steps ADD COLUMN {name} {definition}")
|
|
98
|
+
self._conn.execute(
|
|
99
|
+
"""CREATE TABLE IF NOT EXISTS project_work_orders (
|
|
100
|
+
work_order_ref TEXT PRIMARY KEY,
|
|
101
|
+
work_order_id TEXT NOT NULL UNIQUE,
|
|
102
|
+
work_order_digest TEXT NOT NULL UNIQUE,
|
|
103
|
+
project_id TEXT NOT NULL,
|
|
104
|
+
step_id TEXT NOT NULL,
|
|
105
|
+
schema_version INTEGER NOT NULL,
|
|
106
|
+
payload_json TEXT NOT NULL,
|
|
107
|
+
created_at REAL NOT NULL
|
|
108
|
+
)""")
|
|
109
|
+
self._conn.execute(
|
|
110
|
+
"""CREATE TABLE IF NOT EXISTS project_execution_results (
|
|
111
|
+
result_ref TEXT PRIMARY KEY,
|
|
112
|
+
work_order_ref TEXT NOT NULL UNIQUE,
|
|
113
|
+
work_order_id TEXT NOT NULL UNIQUE,
|
|
114
|
+
work_order_digest TEXT NOT NULL,
|
|
115
|
+
run_id TEXT NOT NULL,
|
|
116
|
+
attempt_number INTEGER NOT NULL,
|
|
117
|
+
terminal_outcome TEXT NOT NULL,
|
|
118
|
+
verification_result TEXT NOT NULL,
|
|
119
|
+
effect_class TEXT NOT NULL,
|
|
120
|
+
payload_json TEXT NOT NULL,
|
|
121
|
+
created_at REAL NOT NULL,
|
|
122
|
+
updated_at REAL NOT NULL
|
|
123
|
+
)""")
|
|
124
|
+
self._conn.execute(
|
|
125
|
+
"""CREATE TABLE IF NOT EXISTS project_execution_attempts (
|
|
126
|
+
attempt_ref TEXT PRIMARY KEY,
|
|
127
|
+
work_order_ref TEXT NOT NULL,
|
|
128
|
+
work_order_id TEXT NOT NULL,
|
|
129
|
+
run_id TEXT NOT NULL,
|
|
130
|
+
attempt_number INTEGER NOT NULL,
|
|
131
|
+
transport_status TEXT NOT NULL,
|
|
132
|
+
result_json TEXT NOT NULL,
|
|
133
|
+
created_at REAL NOT NULL,
|
|
134
|
+
UNIQUE(work_order_id, run_id),
|
|
135
|
+
UNIQUE(work_order_id, attempt_number)
|
|
136
|
+
)""")
|
|
137
|
+
self._conn.execute(
|
|
138
|
+
"""CREATE TABLE IF NOT EXISTS project_event_outbox (
|
|
139
|
+
event_key TEXT PRIMARY KEY,
|
|
140
|
+
event_type TEXT NOT NULL,
|
|
141
|
+
event_digest TEXT NOT NULL,
|
|
142
|
+
payload_json TEXT NOT NULL,
|
|
143
|
+
occurred_at TEXT NOT NULL,
|
|
144
|
+
state TEXT NOT NULL DEFAULT 'pending',
|
|
145
|
+
journal_seq INTEGER,
|
|
146
|
+
journal_event_id TEXT,
|
|
147
|
+
journal_recorded_at TEXT,
|
|
148
|
+
projection_receipt_digest TEXT,
|
|
149
|
+
journal_acknowledged INTEGER NOT NULL DEFAULT 0,
|
|
150
|
+
projection_attempts INTEGER NOT NULL DEFAULT 0,
|
|
151
|
+
last_error TEXT,
|
|
152
|
+
created_at REAL NOT NULL,
|
|
153
|
+
updated_at REAL NOT NULL,
|
|
154
|
+
CHECK (state IN ('pending','projected'))
|
|
155
|
+
)""")
|
|
156
|
+
outbox_cols = {r[1] for r in self._conn.execute(
|
|
157
|
+
"PRAGMA table_info(project_event_outbox)"
|
|
158
|
+
).fetchall()}
|
|
159
|
+
if "journal_acknowledged" not in outbox_cols:
|
|
160
|
+
self._conn.execute(
|
|
161
|
+
"ALTER TABLE project_event_outbox ADD COLUMN "
|
|
162
|
+
"journal_acknowledged INTEGER NOT NULL DEFAULT 0"
|
|
163
|
+
)
|
|
164
|
+
if "projection_receipt_digest" not in outbox_cols:
|
|
165
|
+
self._conn.execute(
|
|
166
|
+
"ALTER TABLE project_event_outbox ADD COLUMN "
|
|
167
|
+
"projection_receipt_digest TEXT"
|
|
168
|
+
)
|
|
169
|
+
self._conn.execute(
|
|
170
|
+
"CREATE INDEX IF NOT EXISTS idx_project_event_outbox_state "
|
|
171
|
+
"ON project_event_outbox(state, created_at, event_key)")
|
|
172
|
+
self._conn.execute(
|
|
173
|
+
"CREATE INDEX IF NOT EXISTS idx_steps_project ON steps(project_id)")
|
|
174
|
+
self._conn.execute(
|
|
175
|
+
"CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)")
|
|
176
|
+
self._conn.execute(
|
|
177
|
+
"CREATE INDEX IF NOT EXISTS idx_projects_goal_fingerprint "
|
|
178
|
+
"ON projects(goal_fingerprint)")
|
|
179
|
+
self._conn.commit()
|
|
180
|
+
|
|
181
|
+
def close(self) -> None:
|
|
182
|
+
with self._lock:
|
|
183
|
+
self._conn.close()
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _json(payload: Dict[str, Any]) -> str:
|
|
187
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"),
|
|
188
|
+
ensure_ascii=False)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _digest(payload: Dict[str, Any]) -> str:
|
|
192
|
+
return hashlib.sha256(
|
|
193
|
+
ProjectStore._json(payload).encode("utf-8")
|
|
194
|
+
).hexdigest()
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def project_event_envelope_digest(
|
|
198
|
+
*, event_key: str, event_type: str, occurred_at: str,
|
|
199
|
+
payload: Dict[str, Any],
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Bind payload and all journal-routing envelope fields together."""
|
|
202
|
+
|
|
203
|
+
return ProjectStore._digest({
|
|
204
|
+
"schema": "ProjectEventEnvelopeV1",
|
|
205
|
+
"version": 1,
|
|
206
|
+
"event_key": str(event_key),
|
|
207
|
+
"event_type": str(event_type),
|
|
208
|
+
"occurred_at": str(occurred_at),
|
|
209
|
+
"payload": payload,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def project_event_projection_receipt_digest(
|
|
214
|
+
*,
|
|
215
|
+
event_key: str,
|
|
216
|
+
event_type: str,
|
|
217
|
+
event_digest: str,
|
|
218
|
+
occurred_at: str,
|
|
219
|
+
journal_seq: int,
|
|
220
|
+
journal_event_id: str,
|
|
221
|
+
journal_recorded_at: str,
|
|
222
|
+
) -> str:
|
|
223
|
+
"""Bind a staged envelope to the exact journal projection receipt."""
|
|
224
|
+
|
|
225
|
+
key = str(event_key or "").strip()
|
|
226
|
+
kind = str(event_type or "").strip()
|
|
227
|
+
envelope_digest = str(event_digest or "").strip()
|
|
228
|
+
occurred = str(occurred_at or "").strip()
|
|
229
|
+
event_id = str(journal_event_id or "").strip()
|
|
230
|
+
recorded = str(journal_recorded_at or "").strip()
|
|
231
|
+
if not key or len(key) > 256 or not kind or len(kind) > 128:
|
|
232
|
+
raise ValueError("journal projection envelope identity is invalid")
|
|
233
|
+
if len(envelope_digest) != 64 or any(
|
|
234
|
+
character not in "0123456789abcdef"
|
|
235
|
+
for character in envelope_digest
|
|
236
|
+
):
|
|
237
|
+
raise ValueError("journal projection envelope digest is invalid")
|
|
238
|
+
if type(journal_seq) is not int or journal_seq < 1:
|
|
239
|
+
raise ValueError("journal projection sequence is invalid")
|
|
240
|
+
if (
|
|
241
|
+
not event_id
|
|
242
|
+
or len(event_id) > 256
|
|
243
|
+
or any(character.isspace() for character in event_id)
|
|
244
|
+
):
|
|
245
|
+
raise ValueError("journal projection event ID is invalid")
|
|
246
|
+
try:
|
|
247
|
+
occurred_time = datetime.fromisoformat(
|
|
248
|
+
occurred.replace("Z", "+00:00")
|
|
249
|
+
)
|
|
250
|
+
recorded_time = datetime.fromisoformat(
|
|
251
|
+
recorded.replace("Z", "+00:00")
|
|
252
|
+
)
|
|
253
|
+
except (TypeError, ValueError) as exc:
|
|
254
|
+
raise ValueError("journal projection timestamps are invalid") from exc
|
|
255
|
+
if occurred_time.tzinfo is None or recorded_time.tzinfo is None:
|
|
256
|
+
raise ValueError("journal projection timestamps require a timezone")
|
|
257
|
+
return ProjectStore._digest({
|
|
258
|
+
"schema": "ProjectEventProjectionReceiptV1",
|
|
259
|
+
"version": 1,
|
|
260
|
+
"event_key": key,
|
|
261
|
+
"event_type": kind,
|
|
262
|
+
"event_digest": envelope_digest,
|
|
263
|
+
"occurred_at": occurred,
|
|
264
|
+
"journal_seq": journal_seq,
|
|
265
|
+
"journal_event_id": event_id,
|
|
266
|
+
"journal_recorded_at": recorded,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _cognition_mode_at_stage() -> str:
|
|
271
|
+
"""Capture the server-controlled evidence mode in the staged record.
|
|
272
|
+
|
|
273
|
+
The reducer may consume the journal much later under a different
|
|
274
|
+
runtime mode. Binding the mode into the immutable outbox payload
|
|
275
|
+
prevents outcomes produced while the legacy writer was authoritative
|
|
276
|
+
from becoming new receipt-derived learning after a live cutover.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
value = os.environ.get(
|
|
280
|
+
"COLONY_COGNITION_EVIDENCE", "off",
|
|
281
|
+
).strip().lower()
|
|
282
|
+
return value if value in {"off", "shadow", "live"} else "off"
|
|
283
|
+
|
|
284
|
+
def _cognition_mode_for_event_locked(
|
|
285
|
+
self, event_key: str,
|
|
286
|
+
) -> Optional[str]:
|
|
287
|
+
"""Return the first-stage mode, never the mode of a later replay."""
|
|
288
|
+
|
|
289
|
+
row = self._conn.execute(
|
|
290
|
+
"SELECT payload_json FROM project_event_outbox WHERE event_key=?",
|
|
291
|
+
(str(event_key),),
|
|
292
|
+
).fetchone()
|
|
293
|
+
if row is None:
|
|
294
|
+
return self._cognition_mode_at_stage()
|
|
295
|
+
try:
|
|
296
|
+
payload = json.loads(str(row["payload_json"]))
|
|
297
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
298
|
+
return None
|
|
299
|
+
if not isinstance(payload, dict):
|
|
300
|
+
return None
|
|
301
|
+
mode = str(payload.get("cognition_mode_at_stage") or "").lower()
|
|
302
|
+
return mode if mode in {"off", "shadow", "live"} else None
|
|
303
|
+
|
|
304
|
+
def _stage_event_locked(
|
|
305
|
+
self,
|
|
306
|
+
*,
|
|
307
|
+
event_key: str,
|
|
308
|
+
event_type: str,
|
|
309
|
+
payload: Dict[str, Any],
|
|
310
|
+
occurred_at: str,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Stage one immutable journal projection in the owning transaction."""
|
|
313
|
+
|
|
314
|
+
key = str(event_key or "").strip()
|
|
315
|
+
kind = str(event_type or "").strip().lower()
|
|
316
|
+
when = str(occurred_at or "").strip()
|
|
317
|
+
if not key or len(key) > 256:
|
|
318
|
+
raise ValueError("project event key is invalid")
|
|
319
|
+
if not kind or len(kind) > 128:
|
|
320
|
+
raise ValueError("project event type is invalid")
|
|
321
|
+
try:
|
|
322
|
+
parsed = datetime.fromisoformat(when.replace("Z", "+00:00"))
|
|
323
|
+
except (TypeError, ValueError) as exc:
|
|
324
|
+
raise ValueError("project event occurred_at is invalid") from exc
|
|
325
|
+
if parsed.tzinfo is None:
|
|
326
|
+
raise ValueError("project event occurred_at requires a timezone")
|
|
327
|
+
encoded = self._json(payload)
|
|
328
|
+
existing = self._conn.execute(
|
|
329
|
+
"SELECT * FROM project_event_outbox WHERE event_key=?", (key,),
|
|
330
|
+
).fetchone()
|
|
331
|
+
if existing is not None:
|
|
332
|
+
digest = self.project_event_envelope_digest(
|
|
333
|
+
event_key=key,
|
|
334
|
+
event_type=kind,
|
|
335
|
+
occurred_at=str(existing["occurred_at"]),
|
|
336
|
+
payload=payload,
|
|
337
|
+
)
|
|
338
|
+
if (
|
|
339
|
+
existing["event_type"] != kind
|
|
340
|
+
or existing["event_digest"] != digest
|
|
341
|
+
or existing["payload_json"] != encoded
|
|
342
|
+
):
|
|
343
|
+
raise ValueError("immutable project event replay mismatch")
|
|
344
|
+
return
|
|
345
|
+
digest = self.project_event_envelope_digest(
|
|
346
|
+
event_key=key,
|
|
347
|
+
event_type=kind,
|
|
348
|
+
occurred_at=when,
|
|
349
|
+
payload=payload,
|
|
350
|
+
)
|
|
351
|
+
stamp = time.time()
|
|
352
|
+
self._conn.execute(
|
|
353
|
+
"""INSERT INTO project_event_outbox (
|
|
354
|
+
event_key,event_type,event_digest,payload_json,occurred_at,
|
|
355
|
+
state,created_at,updated_at
|
|
356
|
+
) VALUES (?,?,?,?,?,'pending',?,?)""",
|
|
357
|
+
(key, kind, digest, encoded, when, stamp, stamp),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _stage_project_terminal_locked(self, project: Project) -> None:
|
|
361
|
+
if project.status not in {"completed", "abandoned"}:
|
|
362
|
+
return
|
|
363
|
+
identity = self._digest({
|
|
364
|
+
"project_id": project.id,
|
|
365
|
+
"lifecycle_status": project.status,
|
|
366
|
+
"outcome": project.outcome,
|
|
367
|
+
})
|
|
368
|
+
event_key = f"project-terminal:{identity}"
|
|
369
|
+
mode_at_stage = self._cognition_mode_for_event_locked(event_key)
|
|
370
|
+
rows = self._conn.execute(
|
|
371
|
+
"SELECT id,status,result_ref FROM steps WHERE project_id=? "
|
|
372
|
+
"ORDER BY ordinal ASC,id ASC",
|
|
373
|
+
(project.id,),
|
|
374
|
+
).fetchall()
|
|
375
|
+
results: list[Dict[str, Any]] = []
|
|
376
|
+
verified_success = bool(rows) and project.outcome == "succeeded"
|
|
377
|
+
for row in rows:
|
|
378
|
+
result_ref = str(row["result_ref"] or "")
|
|
379
|
+
if row["status"] != "done" or not result_ref:
|
|
380
|
+
verified_success = False
|
|
381
|
+
continue
|
|
382
|
+
result_row = self._conn.execute(
|
|
383
|
+
"SELECT * FROM project_execution_results WHERE result_ref=?",
|
|
384
|
+
(result_ref,),
|
|
385
|
+
).fetchone()
|
|
386
|
+
if result_row is None:
|
|
387
|
+
verified_success = False
|
|
388
|
+
continue
|
|
389
|
+
result_payload = json.loads(result_row["payload_json"])
|
|
390
|
+
work_order_row = self._conn.execute(
|
|
391
|
+
"SELECT * FROM project_work_orders WHERE work_order_id=?",
|
|
392
|
+
(result_row["work_order_id"],),
|
|
393
|
+
).fetchone()
|
|
394
|
+
receipt_refs = result_payload.get("receipt_refs") or []
|
|
395
|
+
if (
|
|
396
|
+
work_order_row is None
|
|
397
|
+
or work_order_row["project_id"] != project.id
|
|
398
|
+
or work_order_row["step_id"] != row["id"]
|
|
399
|
+
or work_order_row["work_order_digest"]
|
|
400
|
+
!= result_row["work_order_digest"]
|
|
401
|
+
or result_payload.get("work_order_id")
|
|
402
|
+
!= result_row["work_order_id"]
|
|
403
|
+
or result_payload.get("work_order_digest")
|
|
404
|
+
!= result_row["work_order_digest"]
|
|
405
|
+
or
|
|
406
|
+
result_row["terminal_outcome"] != "succeeded"
|
|
407
|
+
or result_row["verification_result"] != "verified"
|
|
408
|
+
or not isinstance(receipt_refs, list)
|
|
409
|
+
or not receipt_refs
|
|
410
|
+
):
|
|
411
|
+
verified_success = False
|
|
412
|
+
results.append({
|
|
413
|
+
"step_id": str(row["id"]),
|
|
414
|
+
"result_ref": result_ref,
|
|
415
|
+
"result_digest": self._digest(result_payload),
|
|
416
|
+
})
|
|
417
|
+
material = {
|
|
418
|
+
"schema": "ProjectTerminalEvidenceV2",
|
|
419
|
+
"version": 2,
|
|
420
|
+
"project_id": project.id,
|
|
421
|
+
"lifecycle_status": project.status,
|
|
422
|
+
"outcome": project.outcome,
|
|
423
|
+
"status": (
|
|
424
|
+
"verified"
|
|
425
|
+
if verified_success else
|
|
426
|
+
"failed"
|
|
427
|
+
if project.status == "abandoned" else
|
|
428
|
+
"unverified"
|
|
429
|
+
),
|
|
430
|
+
"reason_code": str(project.reason or "")[:128],
|
|
431
|
+
"source": str(project.source or "")[:64],
|
|
432
|
+
"subject_person_id": project.subject_person_id or "owner",
|
|
433
|
+
"viewer_scope": project.viewer_scope or "owner",
|
|
434
|
+
"shareability": project.shareability or "owner_private",
|
|
435
|
+
"concern_id": str(project.concern_id or "")[:128],
|
|
436
|
+
"goal_proposal_id": str(project.goal_proposal_id or "")[:256],
|
|
437
|
+
"result_refs": results,
|
|
438
|
+
"evidence_status": (
|
|
439
|
+
"verified" if verified_success else "unverified"
|
|
440
|
+
),
|
|
441
|
+
}
|
|
442
|
+
if mode_at_stage is not None:
|
|
443
|
+
material["cognition_mode_at_stage"] = mode_at_stage
|
|
444
|
+
material["project_digest"] = self._digest(material)
|
|
445
|
+
self._stage_event_locked(
|
|
446
|
+
event_key=event_key,
|
|
447
|
+
event_type=f"project.{project.status}",
|
|
448
|
+
payload=material,
|
|
449
|
+
occurred_at=datetime.now(timezone.utc).isoformat(),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# -- projects ---------------------------------------------------------
|
|
453
|
+
def insert_authority_bound_project(
|
|
454
|
+
self, p: Project,
|
|
455
|
+
) -> tuple[Project, bool]:
|
|
456
|
+
"""Insert one immutable authority-bound Project, or replay it exactly.
|
|
457
|
+
|
|
458
|
+
Unlike :meth:`save_project`, this operation never upserts. A replay
|
|
459
|
+
returns the persisted lifecycle row byte-for-byte as reconstructed by
|
|
460
|
+
``Project.from_row``; it cannot rewind status, rewrite timestamps, or
|
|
461
|
+
widen any WorkOrder authority field.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
if p.source not in self._AUTHORITY_BOUND_PROJECT_SOURCES:
|
|
465
|
+
raise ValueError("exact project insert requires an authority-bound source")
|
|
466
|
+
row = p.to_row()
|
|
467
|
+
with self._lock:
|
|
468
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
469
|
+
try:
|
|
470
|
+
existing_row = self._conn.execute(
|
|
471
|
+
"SELECT * FROM projects WHERE id=?", (p.id,),
|
|
472
|
+
).fetchone()
|
|
473
|
+
if existing_row is not None:
|
|
474
|
+
existing = Project.from_row(dict(existing_row))
|
|
475
|
+
if any(
|
|
476
|
+
getattr(existing, field) != getattr(p, field)
|
|
477
|
+
for field in self._WORK_ORDER_PROJECT_AUTHORITY_FIELDS
|
|
478
|
+
):
|
|
479
|
+
raise ValueError(
|
|
480
|
+
"immutable authority-bound project replay mismatch"
|
|
481
|
+
)
|
|
482
|
+
self._conn.commit()
|
|
483
|
+
return existing, False
|
|
484
|
+
if (
|
|
485
|
+
p.status != "planning"
|
|
486
|
+
or p.outcome != "pending"
|
|
487
|
+
or p.reason
|
|
488
|
+
or p.replans != 0
|
|
489
|
+
or p.next_review_at != 0.0
|
|
490
|
+
):
|
|
491
|
+
raise ValueError(
|
|
492
|
+
"exact project insert requires an initial planning lifecycle"
|
|
493
|
+
)
|
|
494
|
+
cols = ", ".join(row)
|
|
495
|
+
placeholders = ", ".join(["?"] * len(row))
|
|
496
|
+
self._conn.execute(
|
|
497
|
+
f"INSERT INTO projects ({cols}) VALUES ({placeholders})",
|
|
498
|
+
list(row.values()),
|
|
499
|
+
)
|
|
500
|
+
self._conn.commit()
|
|
501
|
+
return Project.from_row(row), True
|
|
502
|
+
except Exception:
|
|
503
|
+
self._conn.rollback()
|
|
504
|
+
raise
|
|
505
|
+
|
|
506
|
+
def save_project(self, p: Project) -> Project:
|
|
507
|
+
p.updated_at = time.time()
|
|
508
|
+
row = p.to_row()
|
|
509
|
+
with self._lock:
|
|
510
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
511
|
+
try:
|
|
512
|
+
existing_row = self._conn.execute(
|
|
513
|
+
"SELECT * FROM projects WHERE id=?", (p.id,),
|
|
514
|
+
).fetchone()
|
|
515
|
+
if existing_row is not None:
|
|
516
|
+
existing = Project.from_row(dict(existing_row))
|
|
517
|
+
bound_sources = self._AUTHORITY_BOUND_PROJECT_SOURCES
|
|
518
|
+
if existing.source in bound_sources or p.source in bound_sources:
|
|
519
|
+
if any(getattr(existing, field) != getattr(p, field)
|
|
520
|
+
for field in self._AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS):
|
|
521
|
+
if (
|
|
522
|
+
existing.source == "cognition_spine"
|
|
523
|
+
and p.source == "cognition_spine"
|
|
524
|
+
):
|
|
525
|
+
raise ValueError(
|
|
526
|
+
"immutable cognition project provenance mismatch"
|
|
527
|
+
)
|
|
528
|
+
raise ValueError(
|
|
529
|
+
"immutable authority-bound project provenance mismatch"
|
|
530
|
+
)
|
|
531
|
+
cols = ", ".join(row); ph = ", ".join(["?"] * len(row))
|
|
532
|
+
updates = ", ".join(
|
|
533
|
+
f"{k}=excluded.{k}" for k in row if k != "id"
|
|
534
|
+
)
|
|
535
|
+
self._conn.execute(
|
|
536
|
+
f"INSERT INTO projects ({cols}) VALUES ({ph}) "
|
|
537
|
+
f"ON CONFLICT(id) DO UPDATE SET {updates}", list(row.values()))
|
|
538
|
+
self._stage_project_terminal_locked(p)
|
|
539
|
+
self._conn.commit()
|
|
540
|
+
except Exception:
|
|
541
|
+
self._conn.rollback()
|
|
542
|
+
raise
|
|
543
|
+
return p
|
|
544
|
+
|
|
545
|
+
def get_project(self, project_id: str) -> Optional[Project]:
|
|
546
|
+
with self._lock:
|
|
547
|
+
r = self._conn.execute(
|
|
548
|
+
"SELECT * FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
549
|
+
return Project.from_row(dict(r)) if r else None
|
|
550
|
+
|
|
551
|
+
def list_projects(self, status: Optional[str] = None,
|
|
552
|
+
limit: int = 50, offset: int = 0) -> List[Project]:
|
|
553
|
+
q = "SELECT * FROM projects"
|
|
554
|
+
params: List[Any] = []
|
|
555
|
+
if status:
|
|
556
|
+
q += " WHERE status=?"; params.append(status)
|
|
557
|
+
q += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
558
|
+
params.extend((limit, max(0, int(offset))))
|
|
559
|
+
with self._lock:
|
|
560
|
+
rows = self._conn.execute(q, params).fetchall()
|
|
561
|
+
return [Project.from_row(dict(r)) for r in rows]
|
|
562
|
+
|
|
563
|
+
def count(self, status: Optional[str] = None) -> int:
|
|
564
|
+
q = "SELECT COUNT(*) AS n FROM projects"
|
|
565
|
+
params: List[Any] = []
|
|
566
|
+
if status:
|
|
567
|
+
q += " WHERE status=?"; params.append(status)
|
|
568
|
+
with self._lock:
|
|
569
|
+
r = self._conn.execute(q, params).fetchone()
|
|
570
|
+
return int(r["n"])
|
|
571
|
+
|
|
572
|
+
def find_by_goal_fingerprint(self, fingerprint: str) -> Optional[Project]:
|
|
573
|
+
"""Find a non-terminal project for one normalized autonomous goal."""
|
|
574
|
+
|
|
575
|
+
if not str(fingerprint or "").strip():
|
|
576
|
+
return None
|
|
577
|
+
with self._lock:
|
|
578
|
+
row = self._conn.execute(
|
|
579
|
+
"""SELECT * FROM projects WHERE goal_fingerprint=?
|
|
580
|
+
AND status NOT IN ('completed','abandoned')
|
|
581
|
+
ORDER BY created_at ASC LIMIT 1""",
|
|
582
|
+
(str(fingerprint),),
|
|
583
|
+
).fetchone()
|
|
584
|
+
return Project.from_row(dict(row)) if row else None
|
|
585
|
+
|
|
586
|
+
def find_by_source_event_refs(
|
|
587
|
+
self, references: List[str],
|
|
588
|
+
) -> Optional[Project]:
|
|
589
|
+
"""Find the first Project already bound to any exact source event.
|
|
590
|
+
|
|
591
|
+
A transport-attested owner goal may create its Project before the
|
|
592
|
+
ordinary cognition reducer reaches the same journal event. Matching
|
|
593
|
+
the immutable reference prevents that later model pass from creating a
|
|
594
|
+
second Project with different wording and therefore a different goal
|
|
595
|
+
fingerprint.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
wanted = {
|
|
599
|
+
str(reference) for reference in references
|
|
600
|
+
if str(reference or "").strip()
|
|
601
|
+
}
|
|
602
|
+
if not wanted:
|
|
603
|
+
return None
|
|
604
|
+
with self._lock:
|
|
605
|
+
rows = self._conn.execute(
|
|
606
|
+
"SELECT * FROM projects ORDER BY created_at ASC,id ASC"
|
|
607
|
+
).fetchall()
|
|
608
|
+
for row in rows:
|
|
609
|
+
project = Project.from_row(dict(row))
|
|
610
|
+
if wanted.intersection(project.source_event_refs):
|
|
611
|
+
return project
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
def due_for_review(self, now: Optional[float] = None,
|
|
615
|
+
limit: int = 20, offset: int = 0) -> List[Project]:
|
|
616
|
+
"""Active projects whose next_review_at has passed."""
|
|
617
|
+
now = now or time.time()
|
|
618
|
+
with self._lock:
|
|
619
|
+
rows = self._conn.execute(
|
|
620
|
+
"SELECT * FROM projects WHERE status='active' AND "
|
|
621
|
+
"coalesce(next_review_at, 0) <= ? "
|
|
622
|
+
"ORDER BY next_review_at ASC LIMIT ? OFFSET ?",
|
|
623
|
+
(now, limit, max(0, int(offset)))).fetchall()
|
|
624
|
+
return [Project.from_row(dict(r)) for r in rows]
|
|
625
|
+
|
|
626
|
+
# -- steps --------------------------------------------------------------
|
|
627
|
+
def save_step(self, s: Step) -> Step:
|
|
628
|
+
s.updated_at = time.time()
|
|
629
|
+
row = s.to_row()
|
|
630
|
+
with self._lock:
|
|
631
|
+
cols = ", ".join(row); ph = ", ".join(["?"] * len(row))
|
|
632
|
+
updates = ", ".join(f"{k}=excluded.{k}" for k in row if k != "id")
|
|
633
|
+
self._conn.execute(
|
|
634
|
+
f"INSERT INTO steps ({cols}) VALUES ({ph}) "
|
|
635
|
+
f"ON CONFLICT(id) DO UPDATE SET {updates}", list(row.values()))
|
|
636
|
+
self._conn.commit()
|
|
637
|
+
return s
|
|
638
|
+
|
|
639
|
+
def steps_for(self, project_id: str) -> List[Step]:
|
|
640
|
+
with self._lock:
|
|
641
|
+
rows = self._conn.execute(
|
|
642
|
+
"SELECT * FROM steps WHERE project_id=? "
|
|
643
|
+
"ORDER BY ordinal ASC,id ASC",
|
|
644
|
+
(project_id,)).fetchall()
|
|
645
|
+
return [Step.from_row(dict(r)) for r in rows]
|
|
646
|
+
|
|
647
|
+
def delete_steps(self, project_id: str,
|
|
648
|
+
statuses: Optional[List[str]] = None) -> int:
|
|
649
|
+
"""Delete steps of a project (optionally only certain statuses)."""
|
|
650
|
+
q = "DELETE FROM steps WHERE project_id=?"
|
|
651
|
+
params: List[Any] = [project_id]
|
|
652
|
+
if statuses:
|
|
653
|
+
q += f" AND status IN ({','.join(['?'] * len(statuses))})"
|
|
654
|
+
params.extend(statuses)
|
|
655
|
+
with self._lock:
|
|
656
|
+
cur = self._conn.execute(q, params)
|
|
657
|
+
self._conn.commit()
|
|
658
|
+
return cur.rowcount
|
|
659
|
+
|
|
660
|
+
# -- immutable WorkOrder / execution-result ledger --------------------
|
|
661
|
+
def prepare_work_order(self, project: Project, step: Step, order: Any) -> str:
|
|
662
|
+
"""Insert missing parents and bind a WorkOrder without stale upserts.
|
|
663
|
+
|
|
664
|
+
Queue polling may hold an older in-memory Project/Step. This method
|
|
665
|
+
never overwrites an existing lifecycle row: it validates authority
|
|
666
|
+
fields and terminal state, creates the immutable WorkOrder, and binds
|
|
667
|
+
its reference to the step in one SQLite transaction.
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
order.validate()
|
|
671
|
+
if step.project_id != project.id or (
|
|
672
|
+
order.project_id != project.id or order.step_id != step.id
|
|
673
|
+
):
|
|
674
|
+
raise ValueError("WorkOrder parent identity mismatch")
|
|
675
|
+
if project.status in {"completed", "abandoned"} or step.status in {
|
|
676
|
+
"done", "failed", "skipped",
|
|
677
|
+
}:
|
|
678
|
+
raise ValueError("cannot prepare work for a terminal project step")
|
|
679
|
+
ref = f"work-order:{order.work_order_id}"
|
|
680
|
+
payload_json = self._json(order.payload())
|
|
681
|
+
project_row = project.to_row()
|
|
682
|
+
step_row = step.to_row()
|
|
683
|
+
issued_at = datetime.fromisoformat(
|
|
684
|
+
str(order.issued_at).replace("Z", "+00:00")
|
|
685
|
+
).timestamp()
|
|
686
|
+
|
|
687
|
+
with self._lock:
|
|
688
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
689
|
+
try:
|
|
690
|
+
existing_project_row = self._conn.execute(
|
|
691
|
+
"SELECT * FROM projects WHERE id=?", (project.id,),
|
|
692
|
+
).fetchone()
|
|
693
|
+
if existing_project_row is None:
|
|
694
|
+
cols = ", ".join(project_row)
|
|
695
|
+
ph = ", ".join(["?"] * len(project_row))
|
|
696
|
+
self._conn.execute(
|
|
697
|
+
f"INSERT INTO projects ({cols}) VALUES ({ph})",
|
|
698
|
+
list(project_row.values()),
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
existing_project = Project.from_row(
|
|
702
|
+
dict(existing_project_row)
|
|
703
|
+
)
|
|
704
|
+
if existing_project.status in {"completed", "abandoned"}:
|
|
705
|
+
raise ValueError("persisted WorkOrder project is terminal")
|
|
706
|
+
if any(
|
|
707
|
+
getattr(existing_project, field)
|
|
708
|
+
!= getattr(project, field)
|
|
709
|
+
for field in self._WORK_ORDER_PROJECT_AUTHORITY_FIELDS
|
|
710
|
+
) or (
|
|
711
|
+
existing_project.status != project.status
|
|
712
|
+
or existing_project.outcome != project.outcome
|
|
713
|
+
):
|
|
714
|
+
raise ValueError(
|
|
715
|
+
"stale WorkOrder project differs from persisted parent"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
existing_step_row = self._conn.execute(
|
|
719
|
+
"SELECT * FROM steps WHERE id=?", (step.id,),
|
|
720
|
+
).fetchone()
|
|
721
|
+
if existing_step_row is None:
|
|
722
|
+
cols = ", ".join(step_row)
|
|
723
|
+
ph = ", ".join(["?"] * len(step_row))
|
|
724
|
+
self._conn.execute(
|
|
725
|
+
f"INSERT INTO steps ({cols}) VALUES ({ph})",
|
|
726
|
+
list(step_row.values()),
|
|
727
|
+
)
|
|
728
|
+
existing_step = step
|
|
729
|
+
else:
|
|
730
|
+
existing_step = Step.from_row(dict(existing_step_row))
|
|
731
|
+
if existing_step.status in {"done", "failed", "skipped"}:
|
|
732
|
+
raise ValueError("persisted WorkOrder step is terminal")
|
|
733
|
+
step_authority = (
|
|
734
|
+
"project_id", "ordinal", "description", "action_kind",
|
|
735
|
+
"depends_on", "boundary_subject", "confidence",
|
|
736
|
+
)
|
|
737
|
+
if any(
|
|
738
|
+
getattr(existing_step, field) != getattr(step, field)
|
|
739
|
+
for field in step_authority
|
|
740
|
+
) or existing_step.status != step.status:
|
|
741
|
+
raise ValueError(
|
|
742
|
+
"stale WorkOrder step differs from persisted parent"
|
|
743
|
+
)
|
|
744
|
+
stored_issued = float(
|
|
745
|
+
existing_step.work_order_issued_at or 0.0
|
|
746
|
+
)
|
|
747
|
+
if stored_issued and abs(stored_issued - issued_at) > 0.001:
|
|
748
|
+
raise ValueError("persisted WorkOrder issue time mismatch")
|
|
749
|
+
|
|
750
|
+
existing_order = self._conn.execute(
|
|
751
|
+
"SELECT * FROM project_work_orders WHERE work_order_id=?",
|
|
752
|
+
(order.work_order_id,),
|
|
753
|
+
).fetchone()
|
|
754
|
+
if existing_order is not None:
|
|
755
|
+
row = dict(existing_order)
|
|
756
|
+
if (
|
|
757
|
+
row["work_order_digest"] != order.work_order_digest
|
|
758
|
+
or row["payload_json"] != payload_json
|
|
759
|
+
or row["work_order_ref"] != ref
|
|
760
|
+
):
|
|
761
|
+
raise ValueError("immutable WorkOrder replay mismatch")
|
|
762
|
+
else:
|
|
763
|
+
self._conn.execute(
|
|
764
|
+
"""INSERT INTO project_work_orders (
|
|
765
|
+
work_order_ref, work_order_id, work_order_digest,
|
|
766
|
+
project_id, step_id, schema_version, payload_json,
|
|
767
|
+
created_at
|
|
768
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
769
|
+
(
|
|
770
|
+
ref, order.work_order_id, order.work_order_digest,
|
|
771
|
+
order.project_id, order.step_id, int(order.version),
|
|
772
|
+
payload_json, time.time(),
|
|
773
|
+
),
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if (
|
|
777
|
+
existing_step.work_order_ref
|
|
778
|
+
and existing_step.work_order_ref != ref
|
|
779
|
+
) or (
|
|
780
|
+
existing_step.work_order_digest
|
|
781
|
+
and existing_step.work_order_digest
|
|
782
|
+
!= order.work_order_digest
|
|
783
|
+
):
|
|
784
|
+
raise ValueError("step WorkOrder authority replay mismatch")
|
|
785
|
+
changed = self._conn.execute(
|
|
786
|
+
"""UPDATE steps SET work_order_ref=?,work_order_digest=?,
|
|
787
|
+
work_order_issued_at=?,updated_at=?
|
|
788
|
+
WHERE id=? AND status NOT IN ('done','failed','skipped')""",
|
|
789
|
+
(
|
|
790
|
+
ref, order.work_order_digest, issued_at, time.time(),
|
|
791
|
+
step.id,
|
|
792
|
+
),
|
|
793
|
+
).rowcount
|
|
794
|
+
if changed != 1:
|
|
795
|
+
raise ValueError("persisted WorkOrder step became terminal")
|
|
796
|
+
self._conn.commit()
|
|
797
|
+
except Exception:
|
|
798
|
+
self._conn.rollback()
|
|
799
|
+
raise
|
|
800
|
+
return ref
|
|
801
|
+
|
|
802
|
+
def save_work_order(self, order: Any) -> str:
|
|
803
|
+
"""Persist one immutable authority envelope and return its ledger ref."""
|
|
804
|
+
|
|
805
|
+
order.validate()
|
|
806
|
+
ref = f"work-order:{order.work_order_id}"
|
|
807
|
+
payload_json = self._json(order.payload())
|
|
808
|
+
with self._lock:
|
|
809
|
+
existing = self._conn.execute(
|
|
810
|
+
"SELECT * FROM project_work_orders WHERE work_order_id=?",
|
|
811
|
+
(order.work_order_id,),
|
|
812
|
+
).fetchone()
|
|
813
|
+
if existing:
|
|
814
|
+
row = dict(existing)
|
|
815
|
+
if (
|
|
816
|
+
row["work_order_digest"] != order.work_order_digest
|
|
817
|
+
or row["payload_json"] != payload_json
|
|
818
|
+
or row["work_order_ref"] != ref
|
|
819
|
+
):
|
|
820
|
+
raise ValueError("immutable WorkOrder replay mismatch")
|
|
821
|
+
return ref
|
|
822
|
+
self._conn.execute(
|
|
823
|
+
"""INSERT INTO project_work_orders (
|
|
824
|
+
work_order_ref, work_order_id, work_order_digest,
|
|
825
|
+
project_id, step_id, schema_version, payload_json, created_at
|
|
826
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
827
|
+
(
|
|
828
|
+
ref, order.work_order_id, order.work_order_digest,
|
|
829
|
+
order.project_id, order.step_id, int(order.version),
|
|
830
|
+
payload_json, time.time(),
|
|
831
|
+
),
|
|
832
|
+
)
|
|
833
|
+
self._conn.commit()
|
|
834
|
+
return ref
|
|
835
|
+
|
|
836
|
+
def get_work_order(self, ref_or_id: str) -> Optional[Dict[str, Any]]:
|
|
837
|
+
with self._lock:
|
|
838
|
+
row = self._conn.execute(
|
|
839
|
+
"""SELECT * FROM project_work_orders
|
|
840
|
+
WHERE work_order_ref=? OR work_order_id=?""",
|
|
841
|
+
(ref_or_id, ref_or_id),
|
|
842
|
+
).fetchone()
|
|
843
|
+
if not row:
|
|
844
|
+
return None
|
|
845
|
+
result = dict(row)
|
|
846
|
+
result["payload"] = json.loads(result.pop("payload_json"))
|
|
847
|
+
return result
|
|
848
|
+
|
|
849
|
+
def work_orders_awaiting_reconciliation(
|
|
850
|
+
self, *, limit: int = 25,
|
|
851
|
+
) -> List[Dict[str, Any]]:
|
|
852
|
+
"""Return bounded, non-terminal WorkOrders that may need polling.
|
|
853
|
+
|
|
854
|
+
Queue execution and Project pursuit are separate durable lifecycles.
|
|
855
|
+
A Project can become blocked (or ordinary pursuit can be cancelled)
|
|
856
|
+
after its WorkOrder has already reached a terminal queue state. Such
|
|
857
|
+
a result is still an observed fact and must be projected into the
|
|
858
|
+
execution ledger independently of whether the Project is currently
|
|
859
|
+
eligible to advance.
|
|
860
|
+
|
|
861
|
+
This query deliberately includes blocked Projects, but excludes
|
|
862
|
+
terminal Project/Step parents because ``save_execution_result`` must
|
|
863
|
+
never reopen those lifecycles. Rows without a result are ordered
|
|
864
|
+
first so idempotent re-polls cannot starve fresh terminal outcomes.
|
|
865
|
+
"""
|
|
866
|
+
|
|
867
|
+
bound = max(1, min(100, int(limit)))
|
|
868
|
+
with self._lock:
|
|
869
|
+
rows = self._conn.execute(
|
|
870
|
+
"""SELECT wo.*
|
|
871
|
+
FROM project_work_orders wo
|
|
872
|
+
JOIN projects p ON p.id=wo.project_id
|
|
873
|
+
JOIN steps s ON s.id=wo.step_id
|
|
874
|
+
AND s.project_id=wo.project_id
|
|
875
|
+
WHERE p.status NOT IN ('completed','abandoned')
|
|
876
|
+
AND s.status NOT IN ('done','failed','skipped')
|
|
877
|
+
ORDER BY CASE WHEN coalesce(s.result_ref, '') = ''
|
|
878
|
+
THEN 0 ELSE 1 END,
|
|
879
|
+
wo.created_at ASC,wo.work_order_id ASC
|
|
880
|
+
LIMIT ?""",
|
|
881
|
+
(bound,),
|
|
882
|
+
).fetchall()
|
|
883
|
+
results: List[Dict[str, Any]] = []
|
|
884
|
+
for row in rows:
|
|
885
|
+
item = dict(row)
|
|
886
|
+
raw_payload = item.pop("payload_json")
|
|
887
|
+
try:
|
|
888
|
+
item["payload"] = json.loads(raw_payload)
|
|
889
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
890
|
+
# Keep malformed authority addressable so one corrupt row is
|
|
891
|
+
# reported without starving valid rows later in the batch.
|
|
892
|
+
item["payload"] = None
|
|
893
|
+
results.append(item)
|
|
894
|
+
return results
|
|
895
|
+
|
|
896
|
+
def save_execution_result(
|
|
897
|
+
self,
|
|
898
|
+
order: Any,
|
|
899
|
+
result: Any,
|
|
900
|
+
*,
|
|
901
|
+
transport_status: str,
|
|
902
|
+
) -> str:
|
|
903
|
+
"""Append a run attempt and update one stable logical result row.
|
|
904
|
+
|
|
905
|
+
Duplicate delivery of the same run is idempotent. Reusing a run ID
|
|
906
|
+
or attempt number for different content is rejected. Older attempts
|
|
907
|
+
can never overwrite the latest logical result.
|
|
908
|
+
"""
|
|
909
|
+
|
|
910
|
+
order.validate()
|
|
911
|
+
from apsimo.execution_results import (
|
|
912
|
+
EFFECT_CLASSES,
|
|
913
|
+
EXECUTION_RESULT_VERSION,
|
|
914
|
+
TERMINAL_OUTCOMES,
|
|
915
|
+
VERIFICATION_RESULTS,
|
|
916
|
+
bounded_refs,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
if (
|
|
920
|
+
result.schema != "ExecutionResultV1"
|
|
921
|
+
or type(result.version) is not int
|
|
922
|
+
or result.version != EXECUTION_RESULT_VERSION
|
|
923
|
+
):
|
|
924
|
+
raise ValueError("execution result schema/version is invalid")
|
|
925
|
+
if result.work_order_id != order.work_order_id:
|
|
926
|
+
raise ValueError("execution result WorkOrder ID mismatch")
|
|
927
|
+
if result.work_order_digest != order.work_order_digest:
|
|
928
|
+
raise ValueError("execution result WorkOrder digest mismatch")
|
|
929
|
+
if int(result.work_order_version) != int(order.version):
|
|
930
|
+
raise ValueError("execution result WorkOrder version mismatch")
|
|
931
|
+
if result.result_ref != f"execution-result:{order.work_order_id}":
|
|
932
|
+
raise ValueError("execution result reference mismatch")
|
|
933
|
+
if (
|
|
934
|
+
type(result.attempt_number) is not int
|
|
935
|
+
or result.attempt_number < 1
|
|
936
|
+
or result.attempt_number > int(order.max_attempts)
|
|
937
|
+
):
|
|
938
|
+
raise ValueError("execution attempt exceeds WorkOrder budget")
|
|
939
|
+
if result.terminal_outcome not in TERMINAL_OUTCOMES:
|
|
940
|
+
raise ValueError("execution result terminal outcome is invalid")
|
|
941
|
+
if (
|
|
942
|
+
result.effect_class not in EFFECT_CLASSES
|
|
943
|
+
or result.effect_class != order.effect_class
|
|
944
|
+
):
|
|
945
|
+
raise ValueError("execution result effect class mismatch")
|
|
946
|
+
if result.verification_result not in VERIFICATION_RESULTS:
|
|
947
|
+
raise ValueError("execution result verification state is invalid")
|
|
948
|
+
receipts = bounded_refs(result.receipt_refs)
|
|
949
|
+
if receipts != tuple(result.receipt_refs):
|
|
950
|
+
raise ValueError("execution result receipt refs are not canonical")
|
|
951
|
+
if result.terminal_outcome == "succeeded":
|
|
952
|
+
if result.verification_result == "not_applicable":
|
|
953
|
+
raise ValueError("succeeded execution requires verification state")
|
|
954
|
+
if result.verification_result == "verified" and (
|
|
955
|
+
not receipts
|
|
956
|
+
or not str(result.verifier_identity or "").strip()
|
|
957
|
+
or result.verifier_identity == result.executor_identity
|
|
958
|
+
):
|
|
959
|
+
raise ValueError(
|
|
960
|
+
"verified execution requires independent receipt evidence"
|
|
961
|
+
)
|
|
962
|
+
elif result.verification_result != "not_applicable":
|
|
963
|
+
raise ValueError(
|
|
964
|
+
"non-success execution cannot claim success verification"
|
|
965
|
+
)
|
|
966
|
+
for value, field, maximum in (
|
|
967
|
+
(result.run_id, "run_id", 256),
|
|
968
|
+
(result.executor_identity, "executor_identity", 256),
|
|
969
|
+
(result.verifier_identity, "verifier_identity", 256),
|
|
970
|
+
(result.summary, "summary", 4000),
|
|
971
|
+
(result.error, "error", 2000),
|
|
972
|
+
):
|
|
973
|
+
text = str(value or "").strip()
|
|
974
|
+
if len(text) > maximum or (
|
|
975
|
+
field in {"run_id", "executor_identity"} and not text
|
|
976
|
+
):
|
|
977
|
+
raise ValueError(f"execution result {field} is invalid")
|
|
978
|
+
try:
|
|
979
|
+
started_at = datetime.fromisoformat(
|
|
980
|
+
str(result.started_at).replace("Z", "+00:00")
|
|
981
|
+
)
|
|
982
|
+
ended_at = datetime.fromisoformat(
|
|
983
|
+
str(result.ended_at).replace("Z", "+00:00")
|
|
984
|
+
)
|
|
985
|
+
except (TypeError, ValueError) as exc:
|
|
986
|
+
raise ValueError("execution result timestamps are invalid") from exc
|
|
987
|
+
if (
|
|
988
|
+
started_at.tzinfo is None
|
|
989
|
+
or ended_at.tzinfo is None
|
|
990
|
+
or ended_at < started_at
|
|
991
|
+
):
|
|
992
|
+
raise ValueError("execution result timestamps are invalid")
|
|
993
|
+
transport = str(transport_status or "").strip().lower()
|
|
994
|
+
if transport not in {"completed", "neutral", "failed", "cancelled"}:
|
|
995
|
+
raise ValueError("execution result transport status is invalid")
|
|
996
|
+
if transport == "failed" and result.terminal_outcome != "failed":
|
|
997
|
+
raise ValueError("execution result conflicts with failed transport")
|
|
998
|
+
if transport == "cancelled" and result.terminal_outcome != "cancelled":
|
|
999
|
+
raise ValueError("execution result conflicts with cancelled transport")
|
|
1000
|
+
|
|
1001
|
+
work_order_ref = f"work-order:{order.work_order_id}"
|
|
1002
|
+
payload_json = self._json(result.payload())
|
|
1003
|
+
attempt_ref = "execution-attempt:%s" % hashlib.sha256(
|
|
1004
|
+
f"{order.work_order_id}\0{result.run_id}".encode("utf-8")
|
|
1005
|
+
).hexdigest()[:24]
|
|
1006
|
+
now = time.time()
|
|
1007
|
+
|
|
1008
|
+
with self._lock:
|
|
1009
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1010
|
+
try:
|
|
1011
|
+
work_order = self._conn.execute(
|
|
1012
|
+
"""SELECT wo.work_order_digest,wo.project_id,wo.step_id,
|
|
1013
|
+
p.status AS project_status,
|
|
1014
|
+
s.status AS step_status
|
|
1015
|
+
FROM project_work_orders wo
|
|
1016
|
+
JOIN projects p ON p.id=wo.project_id
|
|
1017
|
+
JOIN steps s ON s.id=wo.step_id
|
|
1018
|
+
AND s.project_id=wo.project_id
|
|
1019
|
+
WHERE wo.work_order_ref=?""",
|
|
1020
|
+
(work_order_ref,),
|
|
1021
|
+
).fetchone()
|
|
1022
|
+
if (
|
|
1023
|
+
not work_order
|
|
1024
|
+
or work_order["work_order_digest"] != order.work_order_digest
|
|
1025
|
+
or work_order["project_id"] != order.project_id
|
|
1026
|
+
or work_order["step_id"] != order.step_id
|
|
1027
|
+
):
|
|
1028
|
+
raise ValueError("execution result has no matching WorkOrder ledger row")
|
|
1029
|
+
|
|
1030
|
+
existing_attempt = self._conn.execute(
|
|
1031
|
+
"""SELECT * FROM project_execution_attempts
|
|
1032
|
+
WHERE work_order_id=? AND (run_id=? OR attempt_number=?)""",
|
|
1033
|
+
(order.work_order_id, result.run_id, int(result.attempt_number)),
|
|
1034
|
+
).fetchone()
|
|
1035
|
+
if existing_attempt:
|
|
1036
|
+
row = dict(existing_attempt)
|
|
1037
|
+
if (
|
|
1038
|
+
row["run_id"] != result.run_id
|
|
1039
|
+
or int(row["attempt_number"]) != int(result.attempt_number)
|
|
1040
|
+
or row["transport_status"] != transport
|
|
1041
|
+
or row["result_json"] != payload_json
|
|
1042
|
+
):
|
|
1043
|
+
raise ValueError("execution attempt replay mismatch")
|
|
1044
|
+
else:
|
|
1045
|
+
if (
|
|
1046
|
+
work_order["project_status"] in {
|
|
1047
|
+
"completed", "abandoned",
|
|
1048
|
+
}
|
|
1049
|
+
or work_order["step_status"] in {
|
|
1050
|
+
"done", "failed", "skipped",
|
|
1051
|
+
}
|
|
1052
|
+
):
|
|
1053
|
+
raise ValueError(
|
|
1054
|
+
"new execution attempt cannot mutate a terminal "
|
|
1055
|
+
"project step"
|
|
1056
|
+
)
|
|
1057
|
+
self._conn.execute(
|
|
1058
|
+
"""INSERT INTO project_execution_attempts (
|
|
1059
|
+
attempt_ref, work_order_ref, work_order_id, run_id,
|
|
1060
|
+
attempt_number, transport_status, result_json, created_at
|
|
1061
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1062
|
+
(
|
|
1063
|
+
attempt_ref, work_order_ref, order.work_order_id,
|
|
1064
|
+
result.run_id, int(result.attempt_number),
|
|
1065
|
+
transport, payload_json, now,
|
|
1066
|
+
),
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
logical = self._conn.execute(
|
|
1070
|
+
"SELECT * FROM project_execution_results WHERE result_ref=?",
|
|
1071
|
+
(result.result_ref,),
|
|
1072
|
+
).fetchone()
|
|
1073
|
+
if logical is None:
|
|
1074
|
+
self._conn.execute(
|
|
1075
|
+
"""INSERT INTO project_execution_results (
|
|
1076
|
+
result_ref, work_order_ref, work_order_id,
|
|
1077
|
+
work_order_digest, run_id, attempt_number,
|
|
1078
|
+
terminal_outcome, verification_result, effect_class,
|
|
1079
|
+
payload_json, created_at, updated_at
|
|
1080
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1081
|
+
(
|
|
1082
|
+
result.result_ref, work_order_ref, order.work_order_id,
|
|
1083
|
+
order.work_order_digest, result.run_id,
|
|
1084
|
+
int(result.attempt_number), result.terminal_outcome,
|
|
1085
|
+
result.verification_result, result.effect_class,
|
|
1086
|
+
payload_json, now, now,
|
|
1087
|
+
),
|
|
1088
|
+
)
|
|
1089
|
+
else:
|
|
1090
|
+
current = dict(logical)
|
|
1091
|
+
current_attempt = int(current["attempt_number"])
|
|
1092
|
+
if int(result.attempt_number) == current_attempt:
|
|
1093
|
+
if current["run_id"] != result.run_id or current["payload_json"] != payload_json:
|
|
1094
|
+
raise ValueError("logical execution result replay mismatch")
|
|
1095
|
+
elif int(result.attempt_number) > current_attempt:
|
|
1096
|
+
self._conn.execute(
|
|
1097
|
+
"""UPDATE project_execution_results SET
|
|
1098
|
+
run_id=?, attempt_number=?, terminal_outcome=?,
|
|
1099
|
+
verification_result=?, effect_class=?,
|
|
1100
|
+
payload_json=?, updated_at=? WHERE result_ref=?""",
|
|
1101
|
+
(
|
|
1102
|
+
result.run_id, int(result.attempt_number),
|
|
1103
|
+
result.terminal_outcome, result.verification_result,
|
|
1104
|
+
result.effect_class, payload_json, now,
|
|
1105
|
+
result.result_ref,
|
|
1106
|
+
),
|
|
1107
|
+
)
|
|
1108
|
+
self._conn.execute(
|
|
1109
|
+
"""UPDATE steps SET result_ref=?, work_order_ref=?,
|
|
1110
|
+
work_order_digest=?, updated_at=? WHERE id=?""",
|
|
1111
|
+
(
|
|
1112
|
+
result.result_ref, work_order_ref,
|
|
1113
|
+
order.work_order_digest, now, order.step_id,
|
|
1114
|
+
),
|
|
1115
|
+
)
|
|
1116
|
+
project_row = self._conn.execute(
|
|
1117
|
+
"SELECT subject_person_id,viewer_scope,shareability "
|
|
1118
|
+
"FROM projects WHERE id=?",
|
|
1119
|
+
(order.project_id,),
|
|
1120
|
+
).fetchone()
|
|
1121
|
+
result_payload = result.payload()
|
|
1122
|
+
status = (
|
|
1123
|
+
"verified"
|
|
1124
|
+
if result.terminal_outcome == "succeeded"
|
|
1125
|
+
and result.verification_result == "verified"
|
|
1126
|
+
and bool(result.receipt_refs)
|
|
1127
|
+
else result.terminal_outcome
|
|
1128
|
+
if result.terminal_outcome in {
|
|
1129
|
+
"failed", "cancelled", "skipped",
|
|
1130
|
+
}
|
|
1131
|
+
else "unverified"
|
|
1132
|
+
)
|
|
1133
|
+
identity = self._digest({
|
|
1134
|
+
"work_order_id": order.work_order_id,
|
|
1135
|
+
"run_id": result.run_id,
|
|
1136
|
+
})
|
|
1137
|
+
event_key = f"project-execution:{identity}"
|
|
1138
|
+
mode_at_stage = self._cognition_mode_for_event_locked(event_key)
|
|
1139
|
+
evidence = {
|
|
1140
|
+
"schema": "ProjectExecutionEvidenceV2",
|
|
1141
|
+
"version": 2,
|
|
1142
|
+
"project_id": order.project_id,
|
|
1143
|
+
"step_id": order.step_id,
|
|
1144
|
+
"work_order_id": order.work_order_id,
|
|
1145
|
+
"work_order_digest": order.work_order_digest,
|
|
1146
|
+
"result_ref": result.result_ref,
|
|
1147
|
+
"run_id": result.run_id,
|
|
1148
|
+
"attempt_number": int(result.attempt_number),
|
|
1149
|
+
"terminal_outcome": result.terminal_outcome,
|
|
1150
|
+
"verification_result": result.verification_result,
|
|
1151
|
+
"effect_class": result.effect_class,
|
|
1152
|
+
"transport_status": transport,
|
|
1153
|
+
"status": status,
|
|
1154
|
+
"result_digest": self._digest(result_payload),
|
|
1155
|
+
"receipt_refs": list(result.receipt_refs),
|
|
1156
|
+
"verifier_identity": str(result.verifier_identity or "")[:256],
|
|
1157
|
+
"subject_person_id": (
|
|
1158
|
+
str(project_row["subject_person_id"] or "owner")
|
|
1159
|
+
if project_row is not None else "owner"
|
|
1160
|
+
),
|
|
1161
|
+
"viewer_scope": (
|
|
1162
|
+
str(project_row["viewer_scope"] or "owner")
|
|
1163
|
+
if project_row is not None else "owner"
|
|
1164
|
+
),
|
|
1165
|
+
"shareability": (
|
|
1166
|
+
str(project_row["shareability"] or "owner_private")
|
|
1167
|
+
if project_row is not None else "owner_private"
|
|
1168
|
+
),
|
|
1169
|
+
}
|
|
1170
|
+
if mode_at_stage is not None:
|
|
1171
|
+
evidence["cognition_mode_at_stage"] = mode_at_stage
|
|
1172
|
+
evidence["evidence_digest"] = self._digest(evidence)
|
|
1173
|
+
self._stage_event_locked(
|
|
1174
|
+
event_key=event_key,
|
|
1175
|
+
event_type="work_order.result",
|
|
1176
|
+
payload=evidence,
|
|
1177
|
+
occurred_at=result.ended_at,
|
|
1178
|
+
)
|
|
1179
|
+
self._conn.commit()
|
|
1180
|
+
except Exception:
|
|
1181
|
+
self._conn.rollback()
|
|
1182
|
+
raise
|
|
1183
|
+
return result.result_ref
|
|
1184
|
+
|
|
1185
|
+
def get_execution_result(self, ref_or_work_order_id: str) -> Optional[Dict[str, Any]]:
|
|
1186
|
+
with self._lock:
|
|
1187
|
+
row = self._conn.execute(
|
|
1188
|
+
"""SELECT * FROM project_execution_results
|
|
1189
|
+
WHERE result_ref=? OR work_order_id=?""",
|
|
1190
|
+
(ref_or_work_order_id, ref_or_work_order_id),
|
|
1191
|
+
).fetchone()
|
|
1192
|
+
if not row:
|
|
1193
|
+
return None
|
|
1194
|
+
result = dict(row)
|
|
1195
|
+
result["payload"] = json.loads(result.pop("payload_json"))
|
|
1196
|
+
return result
|
|
1197
|
+
|
|
1198
|
+
def execution_attempts_for(self, work_order_id: str) -> List[Dict[str, Any]]:
|
|
1199
|
+
with self._lock:
|
|
1200
|
+
rows = self._conn.execute(
|
|
1201
|
+
"""SELECT * FROM project_execution_attempts
|
|
1202
|
+
WHERE work_order_id=? ORDER BY attempt_number ASC""",
|
|
1203
|
+
(work_order_id,),
|
|
1204
|
+
).fetchall()
|
|
1205
|
+
results = []
|
|
1206
|
+
for row in rows:
|
|
1207
|
+
item = dict(row)
|
|
1208
|
+
item["result"] = json.loads(item.pop("result_json"))
|
|
1209
|
+
results.append(item)
|
|
1210
|
+
return results
|
|
1211
|
+
|
|
1212
|
+
# -- crash-recoverable host-journal outbox ---------------------------
|
|
1213
|
+
def pending_project_events(self, *, limit: int = 100) -> List[Dict[str, Any]]:
|
|
1214
|
+
bound = max(1, min(500, int(limit)))
|
|
1215
|
+
with self._lock:
|
|
1216
|
+
rows = self._conn.execute(
|
|
1217
|
+
"SELECT * FROM project_event_outbox WHERE state='pending' "
|
|
1218
|
+
"ORDER BY created_at ASC,event_key ASC LIMIT ?",
|
|
1219
|
+
(bound,),
|
|
1220
|
+
).fetchall()
|
|
1221
|
+
events: List[Dict[str, Any]] = []
|
|
1222
|
+
for row in rows:
|
|
1223
|
+
item = dict(row)
|
|
1224
|
+
raw_payload = item.pop("payload_json")
|
|
1225
|
+
try:
|
|
1226
|
+
item["payload"] = json.loads(raw_payload)
|
|
1227
|
+
item["payload_error"] = ""
|
|
1228
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1229
|
+
# Keep the row addressable so the projector can persist a
|
|
1230
|
+
# bounded last_error instead of crashing the entire drain.
|
|
1231
|
+
item["payload"] = None
|
|
1232
|
+
item["payload_error"] = f"invalid project outbox JSON:{exc}"
|
|
1233
|
+
events.append(item)
|
|
1234
|
+
return events
|
|
1235
|
+
|
|
1236
|
+
def unacknowledged_project_events(
|
|
1237
|
+
self, *, limit: int = 100,
|
|
1238
|
+
) -> List[Dict[str, Any]]:
|
|
1239
|
+
bound = max(1, min(500, int(limit)))
|
|
1240
|
+
with self._lock:
|
|
1241
|
+
rows = self._conn.execute(
|
|
1242
|
+
"SELECT * FROM project_event_outbox WHERE state='projected' "
|
|
1243
|
+
"AND journal_acknowledged=0 "
|
|
1244
|
+
"ORDER BY updated_at ASC,event_key ASC LIMIT ?",
|
|
1245
|
+
(bound,),
|
|
1246
|
+
).fetchall()
|
|
1247
|
+
events: List[Dict[str, Any]] = []
|
|
1248
|
+
for row in rows:
|
|
1249
|
+
item = dict(row)
|
|
1250
|
+
raw_payload = item.pop("payload_json")
|
|
1251
|
+
try:
|
|
1252
|
+
item["payload"] = json.loads(raw_payload)
|
|
1253
|
+
item["payload_error"] = ""
|
|
1254
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1255
|
+
item["payload"] = None
|
|
1256
|
+
item["payload_error"] = f"invalid project outbox JSON:{exc}"
|
|
1257
|
+
events.append(item)
|
|
1258
|
+
return events
|
|
1259
|
+
|
|
1260
|
+
def complete_project_event(
|
|
1261
|
+
self,
|
|
1262
|
+
event_key: str,
|
|
1263
|
+
journal_record: Dict[str, Any],
|
|
1264
|
+
) -> Dict[str, Any]:
|
|
1265
|
+
key = str(event_key or "").strip()
|
|
1266
|
+
raw_seq = journal_record.get("seq")
|
|
1267
|
+
raw_event_id = journal_record.get("ulid")
|
|
1268
|
+
raw_recorded_at = journal_record.get("recordedAt")
|
|
1269
|
+
if (
|
|
1270
|
+
not key
|
|
1271
|
+
or type(raw_seq) is not int
|
|
1272
|
+
or type(raw_event_id) is not str
|
|
1273
|
+
or type(raw_recorded_at) is not str
|
|
1274
|
+
):
|
|
1275
|
+
raise ValueError("journal projection receipt is invalid")
|
|
1276
|
+
seq = raw_seq
|
|
1277
|
+
event_id = raw_event_id.strip()
|
|
1278
|
+
recorded_at = raw_recorded_at.strip()
|
|
1279
|
+
if event_id != raw_event_id or recorded_at != raw_recorded_at:
|
|
1280
|
+
raise ValueError("journal projection receipt is not canonical")
|
|
1281
|
+
with self._lock:
|
|
1282
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1283
|
+
try:
|
|
1284
|
+
row = self._conn.execute(
|
|
1285
|
+
"SELECT * FROM project_event_outbox WHERE event_key=?",
|
|
1286
|
+
(key,),
|
|
1287
|
+
).fetchone()
|
|
1288
|
+
if row is None:
|
|
1289
|
+
raise ValueError("project event outbox row is unavailable")
|
|
1290
|
+
try:
|
|
1291
|
+
payload = json.loads(str(row["payload_json"]))
|
|
1292
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1293
|
+
raise ValueError("project event outbox payload is invalid") from exc
|
|
1294
|
+
if not isinstance(payload, dict):
|
|
1295
|
+
raise ValueError("project event outbox payload is invalid")
|
|
1296
|
+
expected_envelope = self.project_event_envelope_digest(
|
|
1297
|
+
event_key=str(row["event_key"]),
|
|
1298
|
+
event_type=str(row["event_type"]),
|
|
1299
|
+
occurred_at=str(row["occurred_at"]),
|
|
1300
|
+
payload=payload,
|
|
1301
|
+
)
|
|
1302
|
+
if str(row["event_digest"] or "") != expected_envelope:
|
|
1303
|
+
raise ValueError("project event outbox envelope digest mismatch")
|
|
1304
|
+
receipt_digest = self.project_event_projection_receipt_digest(
|
|
1305
|
+
event_key=str(row["event_key"]),
|
|
1306
|
+
event_type=str(row["event_type"]),
|
|
1307
|
+
event_digest=expected_envelope,
|
|
1308
|
+
occurred_at=str(row["occurred_at"]),
|
|
1309
|
+
journal_seq=seq,
|
|
1310
|
+
journal_event_id=event_id,
|
|
1311
|
+
journal_recorded_at=recorded_at,
|
|
1312
|
+
)
|
|
1313
|
+
if row["state"] == "projected":
|
|
1314
|
+
if (
|
|
1315
|
+
int(row["journal_seq"] or 0) != seq
|
|
1316
|
+
or str(row["journal_event_id"] or "") != event_id
|
|
1317
|
+
or str(row["journal_recorded_at"] or "") != recorded_at
|
|
1318
|
+
or str(row["projection_receipt_digest"] or "")
|
|
1319
|
+
!= receipt_digest
|
|
1320
|
+
):
|
|
1321
|
+
raise ValueError("project event projection replay mismatch")
|
|
1322
|
+
else:
|
|
1323
|
+
self._conn.execute(
|
|
1324
|
+
"""UPDATE project_event_outbox SET state='projected',
|
|
1325
|
+
journal_seq=?,journal_event_id=?,journal_recorded_at=?,
|
|
1326
|
+
projection_receipt_digest=?,
|
|
1327
|
+
projection_attempts=projection_attempts+1,
|
|
1328
|
+
last_error=NULL,updated_at=? WHERE event_key=?
|
|
1329
|
+
AND state='pending'""",
|
|
1330
|
+
(
|
|
1331
|
+
seq, event_id, recorded_at, receipt_digest,
|
|
1332
|
+
time.time(), key,
|
|
1333
|
+
),
|
|
1334
|
+
)
|
|
1335
|
+
self._conn.commit()
|
|
1336
|
+
except Exception:
|
|
1337
|
+
self._conn.rollback()
|
|
1338
|
+
raise
|
|
1339
|
+
return self.project_event(key) or {}
|
|
1340
|
+
|
|
1341
|
+
def acknowledge_project_event(self, event_key: str) -> None:
|
|
1342
|
+
with self._lock:
|
|
1343
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1344
|
+
try:
|
|
1345
|
+
row = self._conn.execute(
|
|
1346
|
+
"SELECT * FROM project_event_outbox WHERE event_key=?",
|
|
1347
|
+
(str(event_key),),
|
|
1348
|
+
).fetchone()
|
|
1349
|
+
if row is None or row["state"] != "projected":
|
|
1350
|
+
raise ValueError(
|
|
1351
|
+
"project event acknowledgement target is unavailable"
|
|
1352
|
+
)
|
|
1353
|
+
try:
|
|
1354
|
+
payload = json.loads(str(row["payload_json"]))
|
|
1355
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1356
|
+
raise ValueError("project event outbox payload is invalid") from exc
|
|
1357
|
+
if not isinstance(payload, dict):
|
|
1358
|
+
raise ValueError("project event outbox payload is invalid")
|
|
1359
|
+
expected_envelope = self.project_event_envelope_digest(
|
|
1360
|
+
event_key=str(row["event_key"]),
|
|
1361
|
+
event_type=str(row["event_type"]),
|
|
1362
|
+
occurred_at=str(row["occurred_at"]),
|
|
1363
|
+
payload=payload,
|
|
1364
|
+
)
|
|
1365
|
+
if str(row["event_digest"] or "") != expected_envelope:
|
|
1366
|
+
raise ValueError("project event outbox envelope digest mismatch")
|
|
1367
|
+
expected_receipt = self.project_event_projection_receipt_digest(
|
|
1368
|
+
event_key=str(row["event_key"]),
|
|
1369
|
+
event_type=str(row["event_type"]),
|
|
1370
|
+
event_digest=expected_envelope,
|
|
1371
|
+
occurred_at=str(row["occurred_at"]),
|
|
1372
|
+
journal_seq=row["journal_seq"],
|
|
1373
|
+
journal_event_id=str(row["journal_event_id"] or ""),
|
|
1374
|
+
journal_recorded_at=str(row["journal_recorded_at"] or ""),
|
|
1375
|
+
)
|
|
1376
|
+
if (
|
|
1377
|
+
str(row["projection_receipt_digest"] or "")
|
|
1378
|
+
!= expected_receipt
|
|
1379
|
+
):
|
|
1380
|
+
raise ValueError(
|
|
1381
|
+
"project event projection receipt digest mismatch"
|
|
1382
|
+
)
|
|
1383
|
+
changed = self._conn.execute(
|
|
1384
|
+
"""UPDATE project_event_outbox SET journal_acknowledged=1,
|
|
1385
|
+
last_error=NULL,updated_at=? WHERE event_key=?
|
|
1386
|
+
AND state='projected' AND projection_receipt_digest=?""",
|
|
1387
|
+
(time.time(), str(event_key), expected_receipt),
|
|
1388
|
+
).rowcount
|
|
1389
|
+
if changed != 1:
|
|
1390
|
+
raise ValueError(
|
|
1391
|
+
"project event acknowledgement target is unavailable"
|
|
1392
|
+
)
|
|
1393
|
+
self._conn.commit()
|
|
1394
|
+
except Exception:
|
|
1395
|
+
self._conn.rollback()
|
|
1396
|
+
raise
|
|
1397
|
+
|
|
1398
|
+
def fail_project_event(self, event_key: str, error: str) -> None:
|
|
1399
|
+
with self._lock:
|
|
1400
|
+
self._conn.execute(
|
|
1401
|
+
"""UPDATE project_event_outbox SET
|
|
1402
|
+
projection_attempts=projection_attempts+1,last_error=?,updated_at=?
|
|
1403
|
+
WHERE event_key=?""",
|
|
1404
|
+
(str(error or "projection_failed")[:500], time.time(), event_key),
|
|
1405
|
+
)
|
|
1406
|
+
self._conn.commit()
|
|
1407
|
+
|
|
1408
|
+
def project_event(self, event_key: str) -> Optional[Dict[str, Any]]:
|
|
1409
|
+
with self._lock:
|
|
1410
|
+
row = self._conn.execute(
|
|
1411
|
+
"SELECT * FROM project_event_outbox WHERE event_key=?",
|
|
1412
|
+
(str(event_key),),
|
|
1413
|
+
).fetchone()
|
|
1414
|
+
if row is None:
|
|
1415
|
+
return None
|
|
1416
|
+
item = dict(row)
|
|
1417
|
+
raw_payload = item.pop("payload_json")
|
|
1418
|
+
try:
|
|
1419
|
+
item["payload"] = json.loads(raw_payload)
|
|
1420
|
+
item["payload_error"] = ""
|
|
1421
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1422
|
+
item["payload"] = None
|
|
1423
|
+
item["payload_error"] = f"invalid project outbox JSON:{exc}"
|
|
1424
|
+
return item
|
|
1425
|
+
|
|
1426
|
+
def project_event_outbox_status(self) -> Dict[str, Any]:
|
|
1427
|
+
with self._lock:
|
|
1428
|
+
counts = self._conn.execute(
|
|
1429
|
+
"SELECT state,COUNT(*) AS n FROM project_event_outbox "
|
|
1430
|
+
"GROUP BY state"
|
|
1431
|
+
).fetchall()
|
|
1432
|
+
error = self._conn.execute(
|
|
1433
|
+
"SELECT event_key,last_error FROM project_event_outbox "
|
|
1434
|
+
"WHERE last_error IS NOT NULL "
|
|
1435
|
+
"ORDER BY updated_at DESC LIMIT 1"
|
|
1436
|
+
).fetchone()
|
|
1437
|
+
acknowledgement_pending = int(self._conn.execute(
|
|
1438
|
+
"SELECT COUNT(*) AS n FROM project_event_outbox "
|
|
1439
|
+
"WHERE state='projected' AND journal_acknowledged=0"
|
|
1440
|
+
).fetchone()["n"])
|
|
1441
|
+
return {
|
|
1442
|
+
"counts": {row["state"]: int(row["n"]) for row in counts},
|
|
1443
|
+
"acknowledgement_pending": acknowledgement_pending,
|
|
1444
|
+
"last_error": str(error["last_error"] or "") if error else "",
|
|
1445
|
+
"last_error_event_key": str(error["event_key"] or "") if error else "",
|
|
1446
|
+
}
|