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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Import reviewed Hermes DM source quotations without replaying turn effects.
|
|
2
|
+
|
|
3
|
+
Run with --dry-run first. Input must be a stable SQLite backup, not a live WAL
|
|
4
|
+
database. No Hermes runtime, model client, network fetch or ordinary turn API is
|
|
5
|
+
invoked. Unmapped history remains in its original database.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from contextlib import closing
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
import sqlite3
|
|
19
|
+
|
|
20
|
+
from .idempotency import TurnIdempotencyLedger, SourceErased, canonical_turn_digest
|
|
21
|
+
|
|
22
|
+
AUTOMATION = {'cron', 'subagent', 'webhook', 'api_server', 'api', 'cli', 'tool', 'system'}
|
|
23
|
+
OPTIONAL_MESSAGE = {'active': '1', 'compacted': '0', '_compressed_summary': '0',
|
|
24
|
+
'observed': '0', 'tool_calls': 'NULL', 'finish_reason': 'NULL',
|
|
25
|
+
'effect_disposition': 'NULL', 'display_kind': 'NULL', 'platform_message_id': 'NULL'}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mapping_document(path):
|
|
29
|
+
return validate_mapping(json.loads(Path(path).read_text()))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_mapping(value):
|
|
33
|
+
if not isinstance(value, dict) or value.get('version') != 1 or not isinstance(value.get('namespace'), str) or not re.fullmatch(r'[a-zA-Z0-9_.-]{1,80}', value['namespace']):
|
|
34
|
+
raise ValueError('Expected a version 1 mapping with a stable database namespace')
|
|
35
|
+
bindings = value.get('bindings')
|
|
36
|
+
if not isinstance(bindings, list) or not bindings or len(bindings) > 500:
|
|
37
|
+
raise ValueError('Provide 1..500 explicitly reviewed session bindings')
|
|
38
|
+
seen = set()
|
|
39
|
+
for binding in bindings:
|
|
40
|
+
if not isinstance(binding, dict):
|
|
41
|
+
raise ValueError('Every binding must be an object')
|
|
42
|
+
for field in ('session_id', 'platform', 'actor_id', 'chat_id', 'contact_id'):
|
|
43
|
+
if not isinstance(binding.get(field), str) or not binding[field].strip():
|
|
44
|
+
raise ValueError('Every binding needs session, platform, actor, chat and contact IDs')
|
|
45
|
+
if binding['session_id'] in seen:
|
|
46
|
+
raise ValueError('A session cannot have multiple actor bindings')
|
|
47
|
+
seen.add(binding['session_id'])
|
|
48
|
+
if binding['platform'] in AUTOMATION or binding.get('chat_type') != 'dm':
|
|
49
|
+
raise ValueError('This importer accepts explicitly attributed direct messages only')
|
|
50
|
+
if not binding.get('review_evidence'):
|
|
51
|
+
raise ValueError('Every binding requires an independent review evidence reference')
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def open_snapshot(path):
|
|
56
|
+
path = Path(path).resolve(strict=True)
|
|
57
|
+
wal = path.with_name(path.name + '-wal')
|
|
58
|
+
if wal.exists() and wal.stat().st_size:
|
|
59
|
+
raise ValueError('Use a consistent SQLite backup, not an active WAL database')
|
|
60
|
+
with path.open('rb') as stream:
|
|
61
|
+
digest = hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
62
|
+
conn = sqlite3.connect(path.as_uri() + '?mode=ro', uri=True)
|
|
63
|
+
conn.row_factory = sqlite3.Row
|
|
64
|
+
conn.execute('PRAGMA query_only=ON')
|
|
65
|
+
conn.execute('BEGIN')
|
|
66
|
+
return conn, digest
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_bindings(conn, mapping):
|
|
70
|
+
columns = {r['name'] for r in conn.execute('PRAGMA table_info(sessions)')}
|
|
71
|
+
required = {'id', 'source', 'user_id', 'chat_id', 'chat_type'}
|
|
72
|
+
if not required <= columns:
|
|
73
|
+
raise ValueError('Hermes session schema lacks required origin fields; do not infer missing identities')
|
|
74
|
+
messages = {r['name'] for r in conn.execute('PRAGMA table_info(messages)')}
|
|
75
|
+
if not {'id', 'session_id', 'role', 'content', 'timestamp'} <= messages:
|
|
76
|
+
raise ValueError('Unsupported Hermes message schema')
|
|
77
|
+
sessions = {}
|
|
78
|
+
for binding in mapping['bindings']:
|
|
79
|
+
row = conn.execute('SELECT * FROM sessions WHERE id=?', (binding['session_id'],)).fetchone()
|
|
80
|
+
if row is None:
|
|
81
|
+
raise ValueError('A reviewed session is missing from this snapshot')
|
|
82
|
+
row = dict(row)
|
|
83
|
+
for field, actual in (('platform', 'source'), ('actor_id', 'user_id'), ('chat_id', 'chat_id'), ('chat_type', 'chat_type')):
|
|
84
|
+
if row[actual] != binding[field]:
|
|
85
|
+
raise ValueError('A reviewed session no longer matches its exact transport binding')
|
|
86
|
+
origin = row.get('origin_json')
|
|
87
|
+
if origin:
|
|
88
|
+
try:
|
|
89
|
+
origin = json.loads(origin)
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
raise ValueError('Invalid recorded session origin') from None
|
|
92
|
+
if not isinstance(origin, dict):
|
|
93
|
+
raise ValueError('Invalid recorded session origin')
|
|
94
|
+
for field, actual in (('platform', 'platform'), ('user_id', 'actor_id'), ('chat_id', 'chat_id'), ('chat_type', 'chat_type')):
|
|
95
|
+
if origin.get(field) and origin[field] != binding[actual]:
|
|
96
|
+
raise ValueError('Session origin conflicts with the reviewed binding')
|
|
97
|
+
sessions[row['id']] = row
|
|
98
|
+
return sessions, messages
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def source_message(row, session, binding, namespace):
|
|
102
|
+
"""Classify stored messages without treating their role as actor evidence."""
|
|
103
|
+
if row['role'] not in {'user', 'assistant'}:
|
|
104
|
+
return None, 'non_conversation_role'
|
|
105
|
+
if row['_compressed_summary']:
|
|
106
|
+
return None, 'generated_summary'
|
|
107
|
+
if not row['active'] and not row['compacted']:
|
|
108
|
+
return None, 'rewound_or_inactive'
|
|
109
|
+
if row['observed']:
|
|
110
|
+
return None, 'ambient_observation'
|
|
111
|
+
if row['effect_disposition'] or row['display_kind']:
|
|
112
|
+
return None, 'special_message_metadata'
|
|
113
|
+
if row['tool_calls'] not in (None, '', '[]') or row['finish_reason'] in {'tool_calls', 'function_call'}:
|
|
114
|
+
return None, 'tool_call_message'
|
|
115
|
+
content = row['content']
|
|
116
|
+
if not isinstance(content, str) or not content.strip():
|
|
117
|
+
return None, 'empty_or_unsupported_content'
|
|
118
|
+
# Hermes JSON-encodes multimodal block lists in this TEXT column. Retain
|
|
119
|
+
# exact text-only lists; leave attachment history in the original database
|
|
120
|
+
# rather than fetch URLs, flatten blocks, or schedule old vision jobs.
|
|
121
|
+
if content.lstrip().startswith('['):
|
|
122
|
+
try:
|
|
123
|
+
decoded = json.loads(content)
|
|
124
|
+
except ValueError:
|
|
125
|
+
decoded = None
|
|
126
|
+
if isinstance(decoded, list) and decoded and all(isinstance(b, dict) and 'type' in b for b in decoded):
|
|
127
|
+
if any(b['type'] not in {'text', 'input_text', 'output_text'} or not isinstance(b.get('text'), str) for b in decoded):
|
|
128
|
+
return None, 'historical_attachment_unretained'
|
|
129
|
+
content = decoded
|
|
130
|
+
timestamp = row['timestamp']
|
|
131
|
+
try:
|
|
132
|
+
occurred_at = datetime.fromtimestamp(float(timestamp), timezone.utc).isoformat() if math.isfinite(float(timestamp)) else None
|
|
133
|
+
except (ValueError, TypeError, OverflowError, OSError):
|
|
134
|
+
occurred_at = None
|
|
135
|
+
provenance = {'kind': 'hermes_history', 'database_namespace': namespace,
|
|
136
|
+
'message_id': row['id'], 'session_id': row['session_id'],
|
|
137
|
+
'platform': binding['platform'], 'actor_id': binding['actor_id'],
|
|
138
|
+
'chat_id': binding['chat_id'], 'actor_basis': 'reviewed_direct_session',
|
|
139
|
+
'speaker': 'contact' if row['role'] == 'user' else 'assistant',
|
|
140
|
+
'timestamp_basis': 'hermes_recorded_timestamp' if occurred_at else 'unknown',
|
|
141
|
+
'platform_message_id': row['platform_message_id'],
|
|
142
|
+
'session_model_hint': session.get('model'), 'message_model_revision': 'unknown',
|
|
143
|
+
'model_basis': 'session_hint_not_per_message_attribution',
|
|
144
|
+
'review_evidence': binding['review_evidence'], 'learning': 'source_only'}
|
|
145
|
+
return ({'role': row['role'], 'content': content, 'provenance': provenance}, occurred_at), None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _progress(ledger):
|
|
149
|
+
with closing(ledger._connect()) as conn, conn:
|
|
150
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_import_progress (
|
|
151
|
+
run_id TEXT PRIMARY KEY, namespace TEXT NOT NULL, snapshot_sha256 TEXT NOT NULL,
|
|
152
|
+
mapping_sha256 TEXT NOT NULL, cursor INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
counts_json TEXT NOT NULL DEFAULT '{}')''')
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def import_history(database, mapping, *, state_dir=None, apply=False, limit=1000):
|
|
157
|
+
mapping = validate_mapping(mapping)
|
|
158
|
+
if not 1 <= limit <= 10000:
|
|
159
|
+
raise ValueError('Batch limit must be between 1 and 10000 messages')
|
|
160
|
+
if apply and (mapping.get('reviewed') is not True or state_dir is None):
|
|
161
|
+
raise ValueError('Apply requires a reviewed mapping and an explicit target state directory')
|
|
162
|
+
if state_dir is not None and Path(database).resolve() == (Path(state_dir) / 'turn-idempotency.db').resolve():
|
|
163
|
+
raise ValueError('Source snapshot and destination ledger must be different files')
|
|
164
|
+
conn, snapshot_hash = open_snapshot(database)
|
|
165
|
+
with closing(conn):
|
|
166
|
+
sessions, columns = validate_bindings(conn, mapping)
|
|
167
|
+
mapping_hash = canonical_turn_digest(mapping)
|
|
168
|
+
run_id = canonical_turn_digest([mapping['namespace'], snapshot_hash, mapping_hash])
|
|
169
|
+
ledger = TurnIdempotencyLedger(Path(state_dir) / 'turn-idempotency.db') if apply else None
|
|
170
|
+
cursor, counts = 0, Counter()
|
|
171
|
+
if ledger:
|
|
172
|
+
_progress(ledger)
|
|
173
|
+
with closing(ledger._connect()) as progress, progress:
|
|
174
|
+
progress.execute('INSERT OR IGNORE INTO source_import_progress(run_id,namespace,snapshot_sha256,mapping_sha256) VALUES (?,?,?,?)',
|
|
175
|
+
(run_id, mapping['namespace'], snapshot_hash, mapping_hash))
|
|
176
|
+
previous = progress.execute('SELECT cursor,counts_json FROM source_import_progress WHERE run_id=?', (run_id,)).fetchone()
|
|
177
|
+
cursor, counts = previous['cursor'], Counter(json.loads(previous['counts_json']))
|
|
178
|
+
selected = 'id,session_id,role,content,timestamp,' + ','.join(
|
|
179
|
+
key if key in columns else default + ' AS ' + key for key, default in OPTIONAL_MESSAGE.items())
|
|
180
|
+
ids = list(sessions)
|
|
181
|
+
placeholders = ','.join('?' for _ in ids)
|
|
182
|
+
bindings = {b['session_id']: b for b in mapping['bindings']}
|
|
183
|
+
query = f'SELECT {selected} FROM messages WHERE session_id IN ({placeholders}) AND id>? ORDER BY id'
|
|
184
|
+
rows = conn.execute(query + (' LIMIT ?' if apply else ''), (*ids, cursor, limit) if apply else (*ids, cursor))
|
|
185
|
+
for row in rows:
|
|
186
|
+
previous_cursor = cursor
|
|
187
|
+
row = dict(row)
|
|
188
|
+
binding = bindings[row['session_id']]
|
|
189
|
+
prepared, reason = source_message(row, sessions[row['session_id']], binding, mapping['namespace'])
|
|
190
|
+
if reason:
|
|
191
|
+
counts[reason] += 1
|
|
192
|
+
elif ledger:
|
|
193
|
+
message, occurred_at = prepared
|
|
194
|
+
turn_id = f"hermes-history:{mapping['namespace']}:{row['id']}"
|
|
195
|
+
try:
|
|
196
|
+
created = ledger.record_source(turn_id, contact_id=binding['contact_id'],
|
|
197
|
+
session_id=row['session_id'], messages=[message], scope='person',
|
|
198
|
+
occurred_at=occurred_at, derive_claims=False)
|
|
199
|
+
counts['retained_new' if created else 'retained_existing'] += 1
|
|
200
|
+
except SourceErased:
|
|
201
|
+
counts['erased_not_restored'] += 1
|
|
202
|
+
else:
|
|
203
|
+
counts['eligible_source_quotations'] += 1
|
|
204
|
+
cursor = row['id']
|
|
205
|
+
if ledger:
|
|
206
|
+
# Source commit precedes cursor acknowledgement. A crash in
|
|
207
|
+
# between safely repeats record_source, never ordinary effects.
|
|
208
|
+
with closing(ledger._connect()) as progress, progress:
|
|
209
|
+
updated = progress.execute('UPDATE source_import_progress SET cursor=?,counts_json=? WHERE run_id=? AND cursor=?',
|
|
210
|
+
(cursor, json.dumps(counts), run_id, previous_cursor))
|
|
211
|
+
if updated.rowcount != 1:
|
|
212
|
+
raise ValueError('Another importer advanced this run; resume with one process')
|
|
213
|
+
remaining = conn.execute(f'SELECT count(*) FROM messages WHERE session_id IN ({placeholders}) AND id>?', (*ids, cursor)).fetchone()[0]
|
|
214
|
+
unbound = [dict(r) for r in conn.execute(f'''SELECT s.source,count(DISTINCT s.id) sessions,count(m.id) messages
|
|
215
|
+
FROM sessions s LEFT JOIN messages m ON m.session_id=s.id
|
|
216
|
+
WHERE s.id NOT IN ({placeholders}) GROUP BY s.source''', ids)]
|
|
217
|
+
return {'mode': 'apply' if apply else 'dry_run', 'run_id': run_id,
|
|
218
|
+
'snapshot_sha256': snapshot_hash, 'mapping_sha256': mapping_hash,
|
|
219
|
+
'bound_sessions': len(sessions), 'cursor': cursor, 'remaining': remaining,
|
|
220
|
+
'complete': remaining == 0, 'counts': dict(counts),
|
|
221
|
+
'unmapped_retained_in_hermes': unbound,
|
|
222
|
+
'learning_replayed': False, 'models_called': False,
|
|
223
|
+
'projection': 'canonical_source_fts_and_queued_semantic_text_only'}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
228
|
+
parser.add_argument('--database', type=Path, required=True)
|
|
229
|
+
parser.add_argument('--mapping', type=Path, required=True)
|
|
230
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
231
|
+
mode.add_argument('--dry-run', action='store_true')
|
|
232
|
+
mode.add_argument('--apply', action='store_true')
|
|
233
|
+
parser.add_argument('--state-dir', type=Path)
|
|
234
|
+
parser.add_argument('--limit', type=int, default=1000)
|
|
235
|
+
args = parser.parse_args()
|
|
236
|
+
try:
|
|
237
|
+
result = import_history(args.database, mapping_document(args.mapping), state_dir=args.state_dir,
|
|
238
|
+
apply=args.apply, limit=args.limit)
|
|
239
|
+
except (ValueError, OSError, sqlite3.Error) as exc:
|
|
240
|
+
parser.exit(1, f'History import stopped: {exc}\n')
|
|
241
|
+
print(json.dumps(result, indent=2))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == '__main__':
|
|
245
|
+
main()
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Read selected native boards and accepted draft associations.
|
|
2
|
+
|
|
3
|
+
Hermes owns its database, task transitions and recovery. This reader never
|
|
4
|
+
initializes a board or imports the native runtime into the sidecar environment.
|
|
5
|
+
"""
|
|
6
|
+
from contextlib import closing
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import sqlite3
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from .hermes_work import selected_home
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_BOARD = re.compile(r'[a-z0-9][a-z0-9_-]{0,63}')
|
|
19
|
+
_TERMINAL = ('done', 'cancelled', 'archived')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _board_path(home, board):
|
|
23
|
+
return home/'kanban.db' if board == 'default' else home/'kanban/boards'/board/'kanban.db'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def observed_boards():
|
|
27
|
+
"""Select explicit boards or the native current namespace, never enumerate."""
|
|
28
|
+
home = selected_home()
|
|
29
|
+
if home is None:
|
|
30
|
+
return None, [], 'profile_not_bound'
|
|
31
|
+
if home.parent.name == 'profiles':
|
|
32
|
+
home = home.parent.parent
|
|
33
|
+
override = os.environ.get('HERMES_KANBAN_HOME', '').strip()
|
|
34
|
+
if override and Path(override).expanduser().resolve() != home:
|
|
35
|
+
raise ValueError('conflicting_native_home')
|
|
36
|
+
configured = os.environ.get('COLONY_HERMES_WORK_BOARDS')
|
|
37
|
+
if configured is not None:
|
|
38
|
+
boards = json.loads(configured)
|
|
39
|
+
if (not isinstance(boards, list) or not 1 <= len(boards) <= 8
|
|
40
|
+
or any(not isinstance(b, str) or not _BOARD.fullmatch(b) for b in boards)):
|
|
41
|
+
raise ValueError('invalid_selected_boards')
|
|
42
|
+
boards = list(dict.fromkeys(boards))
|
|
43
|
+
selection = 'configured_boards'
|
|
44
|
+
else:
|
|
45
|
+
# Match native current-board precedence. Stale native selections fall
|
|
46
|
+
# through to default; explicit observer selections remain unavailable.
|
|
47
|
+
candidates = [os.environ.get('HERMES_KANBAN_BOARD', '').strip().lower()]
|
|
48
|
+
pointer = home/'kanban/current'
|
|
49
|
+
if pointer.is_file() and pointer.stat().st_size <= 256:
|
|
50
|
+
candidates.append(pointer.read_text().strip().lower())
|
|
51
|
+
boards = [next((b for b in candidates if _BOARD.fullmatch(b) and
|
|
52
|
+
(b == 'default' or _board_path(home, b).exists()
|
|
53
|
+
or (_board_path(home, b).parent/'board.json').exists())), 'default')]
|
|
54
|
+
selection = 'current_board_only'
|
|
55
|
+
pinned = os.environ.get('HERMES_KANBAN_DB', '').strip()
|
|
56
|
+
if pinned and (len(boards) != 1 or Path(pinned).expanduser().resolve()
|
|
57
|
+
!= _board_path(home, boards[0]).resolve()):
|
|
58
|
+
raise ValueError('conflicting_native_board')
|
|
59
|
+
return home, boards, selection
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def kanban_view(*, limit=8, now=None, read_budget=.2):
|
|
63
|
+
"""Owner-only operational projection; no board initialization or dispatch."""
|
|
64
|
+
view = {'source': 'hermes_native_kanban_ledgers', 'available': False,
|
|
65
|
+
'items': [], 'recent': [], 'complete': False, 'boards': [],
|
|
66
|
+
'coverage': 'selected native boards only; task/run records and goal budgets, '
|
|
67
|
+
'not process liveness or verified external effects'}
|
|
68
|
+
try:
|
|
69
|
+
home, boards, selection = observed_boards()
|
|
70
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
71
|
+
return {**view, 'reason': 'invalid_native_board_binding'}
|
|
72
|
+
if home is None:
|
|
73
|
+
return {**view, 'reason': selection}
|
|
74
|
+
view.update(source_home_id=hashlib.sha256(str(home).encode()).hexdigest(), selection=selection)
|
|
75
|
+
limit = max(1, min(int(limit), 100))
|
|
76
|
+
now = time.time() if now is None else now
|
|
77
|
+
deadline = time.monotonic() + min(.2, max(.001, read_budget))
|
|
78
|
+
active, recent, total, recent_total = [], [], 0, 0
|
|
79
|
+
for board in boards:
|
|
80
|
+
coverage = {'board': board, 'available': False}
|
|
81
|
+
view['boards'].append(coverage)
|
|
82
|
+
path = _board_path(home, board)
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
coverage['reason'] = 'native_board_absent'
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
remaining = deadline - time.monotonic()
|
|
88
|
+
if remaining <= 0:
|
|
89
|
+
raise TimeoutError
|
|
90
|
+
with closing(sqlite3.connect(path.as_uri()+'?mode=ro', uri=True,
|
|
91
|
+
timeout=min(.05, remaining))) as db:
|
|
92
|
+
db.row_factory = sqlite3.Row
|
|
93
|
+
db.execute('PRAGMA query_only=ON')
|
|
94
|
+
db.set_progress_handler(lambda: int(time.monotonic() >= deadline), 1000)
|
|
95
|
+
db.execute('BEGIN')
|
|
96
|
+
terminal = "status IN ('done','cancelled','archived')"
|
|
97
|
+
# Archiving an unfinished task records an event, not a
|
|
98
|
+
# completion. Preserve that distinction in the projection.
|
|
99
|
+
terminal_at = ("CASE WHEN status='archived' THEN "
|
|
100
|
+
"(SELECT MAX(e.created_at) FROM task_events e "
|
|
101
|
+
"WHERE e.task_id=tasks.id AND e.kind='archived') "
|
|
102
|
+
"ELSE completed_at END")
|
|
103
|
+
active_count = db.execute(f'SELECT count(*) FROM tasks WHERE NOT ({terminal})').fetchone()[0]
|
|
104
|
+
recent_count = db.execute(f'SELECT count(*) FROM tasks WHERE {terminal} AND ({terminal_at})>=?', (now-7*86400,)).fetchone()[0]
|
|
105
|
+
columns = ('id,title,status,assignee,goal_mode,goal_max_turns,current_run_id,'
|
|
106
|
+
'created_at,started_at,completed_at,last_heartbeat_at,'
|
|
107
|
+
f'{terminal_at} AS terminal_record_at')
|
|
108
|
+
rows = db.execute(f'SELECT {columns} FROM tasks WHERE NOT ({terminal}) '
|
|
109
|
+
'ORDER BY created_at DESC,id LIMIT ?', (limit,)).fetchall()
|
|
110
|
+
done = db.execute(f'SELECT {columns} FROM tasks WHERE {terminal} AND ({terminal_at})>=? '
|
|
111
|
+
'ORDER BY terminal_record_at DESC,id LIMIT ?', (now-7*86400,limit)).fetchall()
|
|
112
|
+
|
|
113
|
+
def project(row):
|
|
114
|
+
run = db.execute('SELECT status FROM task_runs WHERE id=? AND task_id=?',
|
|
115
|
+
(row['current_run_id'], row['id'])).fetchone()
|
|
116
|
+
heartbeat = row['last_heartbeat_at']
|
|
117
|
+
return {'native_board': board, 'native_task_id': row['id'],
|
|
118
|
+
'label': str(row['title'] or '')[:128], 'status': row['status'],
|
|
119
|
+
'assignee': str(row['assignee'] or '')[:128],
|
|
120
|
+
'goal_mode': bool(row['goal_mode']), 'goal_max_turns': row['goal_max_turns'],
|
|
121
|
+
'native_run_id': row['current_run_id'], 'native_run_status': run['status'] if run else None,
|
|
122
|
+
'created_at': row['created_at'], 'started_at': row['started_at'],
|
|
123
|
+
'completed_at': row['completed_at'],
|
|
124
|
+
'terminal_record_at': row['terminal_record_at'],
|
|
125
|
+
'heartbeat_age_seconds': round(max(0., now-heartbeat), 1) if heartbeat else None,
|
|
126
|
+
'liveness': 'native_terminal_record' if row['status'] in _TERMINAL else 'unknown'}
|
|
127
|
+
projected, projected_done = [project(r) for r in rows], [project(r) for r in done]
|
|
128
|
+
active.extend(projected); recent.extend(projected_done)
|
|
129
|
+
total += active_count; recent_total += recent_count
|
|
130
|
+
coverage.update(available=True, total=active_count, recent_total=recent_count)
|
|
131
|
+
except (OSError, sqlite3.Error, ValueError, TypeError):
|
|
132
|
+
coverage['reason'] = 'native_board_unavailable'
|
|
133
|
+
active.sort(key=lambda r: (-r['created_at'], r['native_board'], r['native_task_id']))
|
|
134
|
+
recent.sort(key=lambda r: (-r['terminal_record_at'], r['native_board'], r['native_task_id']))
|
|
135
|
+
available = any(b['available'] for b in view['boards'])
|
|
136
|
+
return {**view, 'available': available, 'partial': not all(b['available'] for b in view['boards']),
|
|
137
|
+
**({} if available else {'reason': 'selected_native_boards_unavailable'}),
|
|
138
|
+
'items': active[:limit], 'recent': recent[:limit], 'total': total,
|
|
139
|
+
'recent_total': recent_total, 'truncated': total>limit, 'recent_truncated': recent_total>limit,
|
|
140
|
+
'limit': limit, 'recent_window_seconds': 7*86400}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def selected_board():
|
|
144
|
+
home = selected_home()
|
|
145
|
+
if home is not None and home.parent.name == 'profiles':
|
|
146
|
+
home = home.parent.parent
|
|
147
|
+
board = os.environ.get('COLONY_LOCAL_WORK_BOARD', 'colony-drafts').strip()
|
|
148
|
+
profile = os.environ.get('COLONY_LOCAL_WORK_PROFILE', 'colony-drafts').strip()
|
|
149
|
+
if home is None or not re.fullmatch(r'[a-z0-9][a-z0-9_-]{0,63}', board) or not profile:
|
|
150
|
+
raise ValueError('native_board_binding_unavailable')
|
|
151
|
+
path = home/'kanban.db' if board == 'default' else home/'kanban/boards'/board/'kanban.db'
|
|
152
|
+
return home, board, profile, path
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def task_snapshot(identifier, contact_id, native, *, review=False, followup=False, read_timeout=.2):
|
|
156
|
+
"""Verify native provenance and, when supplied, the currently held run."""
|
|
157
|
+
if review or followup:
|
|
158
|
+
home, boards, _ = observed_boards()
|
|
159
|
+
if home is None or 'default' not in boards:
|
|
160
|
+
raise ValueError('selected_native_review_board_required')
|
|
161
|
+
board, profile, path = 'default', 'default', _board_path(home, 'default')
|
|
162
|
+
else:
|
|
163
|
+
home, board, profile, path = selected_board()
|
|
164
|
+
if native['native_board'] != board:
|
|
165
|
+
raise ValueError('selected_native_board_required')
|
|
166
|
+
if not path.is_file():
|
|
167
|
+
raise OSError('native_board_unavailable')
|
|
168
|
+
with closing(sqlite3.connect(path.as_uri()+'?mode=ro', uri=True,
|
|
169
|
+
timeout=min(2., max(0., read_timeout)))) as db:
|
|
170
|
+
db.row_factory = sqlite3.Row
|
|
171
|
+
db.execute('PRAGMA query_only=ON')
|
|
172
|
+
db.execute('BEGIN')
|
|
173
|
+
task = db.execute('SELECT * FROM tasks WHERE id=?', (native['native_task_id'],)).fetchone()
|
|
174
|
+
if review:
|
|
175
|
+
profile = 'colony-reviews'
|
|
176
|
+
# Preserve historical observations; an unrestricted never-run task
|
|
177
|
+
# cannot be associated through this API after the boundary change.
|
|
178
|
+
if task is not None and task['assignee'] == 'default' and db.execute(
|
|
179
|
+
'SELECT 1 FROM task_runs WHERE task_id=? LIMIT 1', (task['id'],)).fetchone():
|
|
180
|
+
profile = 'default'
|
|
181
|
+
creator = 'colony-followup' if followup else 'colony-initiative' if review else 'colony-local-work'
|
|
182
|
+
if (task is None or task['created_by'] != creator
|
|
183
|
+
or task['idempotency_key'] != creator+':'+identifier
|
|
184
|
+
or task['tenant'] != contact_id or task['assignee'] != profile):
|
|
185
|
+
raise ValueError('accepted_native_task_required')
|
|
186
|
+
result = {'source_home_id': hashlib.sha256(str(home).encode()).hexdigest(),
|
|
187
|
+
'native_board': board, 'native_task_id': task['id']}
|
|
188
|
+
count = db.execute('SELECT count(*) FROM task_runs WHERE task_id=?', (task['id'],)).fetchone()[0]
|
|
189
|
+
run = None
|
|
190
|
+
if native.get('native_run_id') is not None:
|
|
191
|
+
run = db.execute('SELECT * FROM task_runs WHERE id=? AND task_id=?',
|
|
192
|
+
(native['native_run_id'], task['id'])).fetchone()
|
|
193
|
+
if (task['status'] != 'running' or task['current_run_id'] != native['native_run_id']
|
|
194
|
+
or run is None or run['status'] != 'running'
|
|
195
|
+
or not native.get('native_claim_lock')
|
|
196
|
+
or task['claim_lock'] != native['native_claim_lock']
|
|
197
|
+
or run['claim_lock'] != native['native_claim_lock']):
|
|
198
|
+
raise ValueError('current_native_run_required')
|
|
199
|
+
result.update(native_run_id=run['id'], native_claim_lock=run['claim_lock'])
|
|
200
|
+
state = {'status': task['status'], 'native_run_id': task['current_run_id'],
|
|
201
|
+
'attempt_count': count, 'archived': task['status'] == 'archived'}
|
|
202
|
+
if review or followup:
|
|
203
|
+
state['forecast_configuration'] = {
|
|
204
|
+
'runtime_budget_seconds': dict(task).get('max_runtime_seconds'),
|
|
205
|
+
'requested_profile': task['assignee'],
|
|
206
|
+
'task_model_override_at_observation': dict(task).get('model_override'),
|
|
207
|
+
'task_provider_override_at_observation': dict(task).get('provider_override'),
|
|
208
|
+
'served_model': None, 'timestamp_precision_seconds': 1,
|
|
209
|
+
}
|
|
210
|
+
latest = db.execute('SELECT * FROM task_runs WHERE task_id=? ORDER BY id DESC LIMIT 1',
|
|
211
|
+
(task['id'],)).fetchone()
|
|
212
|
+
gave_up = bool(latest and db.execute("SELECT 1 FROM task_events WHERE task_id=? "
|
|
213
|
+
"AND kind='gave_up' AND created_at>=? LIMIT 1", (task['id'], latest['started_at'])).fetchone())
|
|
214
|
+
state.update(contract_sha256=hashlib.sha256((task['body'] or '').encode()).hexdigest(),
|
|
215
|
+
completed_run=bool(latest and latest['outcome'] == 'completed'),
|
|
216
|
+
gave_up=gave_up and task['status'] == 'blocked' and latest['outcome'] in
|
|
217
|
+
{'gave_up', 'crashed', 'timed_out', 'spawn_failed'},
|
|
218
|
+
run_outcome=latest['outcome'] if latest else None,
|
|
219
|
+
error=str(latest['error'] or task['last_failure_error'] or '')[:500] if latest else '',
|
|
220
|
+
summary=str(latest['summary'] or '')[:1600] if latest else '',
|
|
221
|
+
native_run_id=latest['id'] if latest else None)
|
|
222
|
+
terminal_at = dict(task).get('completed_at')
|
|
223
|
+
if task['status'] == 'archived':
|
|
224
|
+
terminal_at = db.execute("SELECT MAX(created_at) FROM task_events WHERE task_id=? AND kind='archived'", (task['id'],)).fetchone()[0]
|
|
225
|
+
if latest and latest['ended_at'] is not None:
|
|
226
|
+
state['duration_observation'] = {
|
|
227
|
+
'outcome': latest['outcome'], 'started_at': latest['started_at'],
|
|
228
|
+
'ended_at': latest['ended_at'], 'attempt_count': count,
|
|
229
|
+
'profile': latest['profile'], 'served_model': None,
|
|
230
|
+
'execution_seconds': latest['ended_at'] - latest['started_at'],
|
|
231
|
+
'timestamp_precision_seconds': 1,
|
|
232
|
+
}
|
|
233
|
+
elif terminal_at is not None and task['status'] in {'done', 'archived', 'cancelled'}:
|
|
234
|
+
state['duration_observation'] = {
|
|
235
|
+
'outcome': task['status'], 'started_at': dict(task).get('started_at'),
|
|
236
|
+
'ended_at': terminal_at, 'attempt_count': count,
|
|
237
|
+
'profile': task['assignee'], 'served_model': None,
|
|
238
|
+
'execution_seconds': None, 'timestamp_precision_seconds': 1,
|
|
239
|
+
}
|
|
240
|
+
if latest and latest['outcome'] in {'crashed', 'timed_out', 'spawn_failed', 'gave_up'}:
|
|
241
|
+
state['runtime_observation'] = {
|
|
242
|
+
'outcome': latest['outcome'], 'started_at': latest['started_at'],
|
|
243
|
+
'ended_at': latest['ended_at'], 'max_runtime_seconds': latest['max_runtime_seconds'],
|
|
244
|
+
'attempt_count': count, 'profile': latest['profile'],
|
|
245
|
+
'task_model_override_at_observation': dict(task).get('model_override'),
|
|
246
|
+
'task_provider_override_at_observation': dict(task).get('provider_override'),
|
|
247
|
+
'served_model': 'unknown',
|
|
248
|
+
'role': 'native_default_worker',
|
|
249
|
+
}
|
|
250
|
+
return result, state
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def project_accepted(identifier, contact_id, context):
|
|
254
|
+
"""Observe only an accepted task, never expose a machine-wide board."""
|
|
255
|
+
review = context.get('native_review')
|
|
256
|
+
if review:
|
|
257
|
+
contact_id, context = review['contact_id'], {**review, 'execution_backend': 'kanban'}
|
|
258
|
+
if context.get('execution_backend') != 'kanban' or not context.get('native_task_id'):
|
|
259
|
+
return None
|
|
260
|
+
try:
|
|
261
|
+
native, state = task_snapshot(identifier, contact_id, {
|
|
262
|
+
'native_board': context.get('native_board'), 'native_task_id': context['native_task_id']}, review=bool(review))
|
|
263
|
+
if native['source_home_id'] != context.get('source_home_id'):
|
|
264
|
+
raise ValueError('selected_native_home_changed')
|
|
265
|
+
return {'available': True, **native, **state,
|
|
266
|
+
'liveness': 'native_running_record' if state['status'] == 'running' else 'native_task_record'}
|
|
267
|
+
except (OSError, sqlite3.Error, ValueError, KeyError, TypeError):
|
|
268
|
+
return {'available': False, 'reason': 'accepted_native_task_unavailable'}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Read one explicitly bound native cron ledger, without importing its scheduler.
|
|
2
|
+
|
|
3
|
+
Hermes owns transitions and recovery. These rows are a read projection, not a
|
|
4
|
+
new lease, process inventory, or proof that an external effect completed.
|
|
5
|
+
"""
|
|
6
|
+
from contextlib import closing
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import sqlite3
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from apsimo import get_state_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def selected_home():
|
|
19
|
+
"""Use the private instance binding or explicit HERMES_HOME, never a scan."""
|
|
20
|
+
selected = os.environ.get('HERMES_HOME', '').strip()
|
|
21
|
+
path = get_state_dir() / 'instance.json'
|
|
22
|
+
if path.is_file():
|
|
23
|
+
manifest = json.loads(path.read_text())
|
|
24
|
+
if manifest.get('version') != 1 or manifest.get('profile') != 'local':
|
|
25
|
+
raise ValueError('unsupported_instance_binding')
|
|
26
|
+
bound = Path(manifest['hermes_home']).expanduser().resolve()
|
|
27
|
+
if selected and Path(selected).expanduser().resolve() != bound:
|
|
28
|
+
raise ValueError('conflicting_instance_binding')
|
|
29
|
+
return bound
|
|
30
|
+
return Path(selected).expanduser().resolve() if selected else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _age(value, now):
|
|
34
|
+
if not value:
|
|
35
|
+
return None
|
|
36
|
+
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
37
|
+
if parsed.tzinfo is None:
|
|
38
|
+
return None
|
|
39
|
+
return round(max(0., now - parsed.timestamp()), 1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cron_view(*, limit=8, now=None):
|
|
43
|
+
view = {'source': 'hermes_native_cron_ledger', 'available': False,
|
|
44
|
+
'items': [], 'recent': [], 'complete': False,
|
|
45
|
+
'coverage': 'one selected Hermes profile; no process liveness or external-effect verification'}
|
|
46
|
+
try:
|
|
47
|
+
home = selected_home()
|
|
48
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
49
|
+
return {**view, 'reason': 'invalid_profile_binding'}
|
|
50
|
+
if home is None:
|
|
51
|
+
return {**view, 'reason': 'profile_not_bound'}
|
|
52
|
+
view['source_home_id'] = hashlib.sha256(str(home).encode()).hexdigest()
|
|
53
|
+
path = home / 'cron' / 'executions.db'
|
|
54
|
+
limit = max(1, min(int(limit), 100))
|
|
55
|
+
now = time.time() if now is None else now
|
|
56
|
+
deadline = time.monotonic() + .2
|
|
57
|
+
try:
|
|
58
|
+
if not path.is_file():
|
|
59
|
+
return {**view, 'reason': 'native_ledger_absent'}
|
|
60
|
+
with closing(sqlite3.connect(path.as_uri() + '?mode=ro', uri=True, timeout=.1)) as conn:
|
|
61
|
+
conn.row_factory = sqlite3.Row
|
|
62
|
+
conn.execute('PRAGMA query_only=ON')
|
|
63
|
+
conn.set_progress_handler(lambda: int(time.monotonic() >= deadline), 1000)
|
|
64
|
+
# One snapshot for active counts and rows. No initialization,
|
|
65
|
+
# recovery, native list_executions(), or schema writes on read.
|
|
66
|
+
conn.execute('BEGIN')
|
|
67
|
+
total = conn.execute("SELECT count(*) FROM executions WHERE status IN ('claimed','running')").fetchone()[0]
|
|
68
|
+
columns = 'id, job_id, source, status, claimed_at, started_at, finished_at'
|
|
69
|
+
active = conn.execute(f"SELECT {columns} FROM executions WHERE status IN ('claimed','running') ORDER BY claimed_at DESC, id DESC LIMIT ?", (limit,)).fetchall()
|
|
70
|
+
cutoff = datetime.fromtimestamp(now - 86400, timezone.utc).isoformat()
|
|
71
|
+
recent = conn.execute(f"SELECT {columns} FROM executions WHERE status IN ('completed','failed','unknown') AND julianday(finished_at) >= julianday(?) ORDER BY finished_at DESC, id DESC LIMIT ?", (cutoff, limit)).fetchall()
|
|
72
|
+
# Labels are optional, bounded, and never fall back to prompts/scripts.
|
|
73
|
+
names = {}
|
|
74
|
+
jobs = home / 'cron' / 'jobs.json'
|
|
75
|
+
if jobs.is_file() and jobs.stat().st_size <= 4 * 1024 * 1024:
|
|
76
|
+
try:
|
|
77
|
+
document = json.loads(jobs.read_text())
|
|
78
|
+
names = {str(job['id']): str(job['name'])[:200]
|
|
79
|
+
for job in document.get('jobs', [])
|
|
80
|
+
if isinstance(job, dict) and job.get('id') and isinstance(job.get('name'), str)}
|
|
81
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def project(row):
|
|
85
|
+
item = dict(row)
|
|
86
|
+
item['execution_id'] = item.pop('id')
|
|
87
|
+
item['name'] = names.get(item['job_id'])
|
|
88
|
+
item['record_age_seconds'] = _age(item['finished_at'] or item['started_at'] or item['claimed_at'], now)
|
|
89
|
+
item['liveness'] = 'unknown' if item['status'] in {'claimed', 'running', 'unknown'} else 'native_terminal_record'
|
|
90
|
+
return item
|
|
91
|
+
|
|
92
|
+
return {**view, 'available': True, 'items': [project(row) for row in active],
|
|
93
|
+
'recent': [project(row) for row in recent], 'total': total,
|
|
94
|
+
'truncated': total > len(active), 'recent_window_seconds': 86400}
|
|
95
|
+
except (OSError, sqlite3.Error, ValueError, TypeError):
|
|
96
|
+
return {**view, 'reason': 'native_ledger_unavailable'}
|