marvisx-cli 0.1.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.
- core/api/__init__.py +0 -0
- core/api/agents/__init__.py +0 -0
- core/api/agents/session_health.py +59 -0
- core/api/agents/session_manager.py +206 -0
- core/api/bin/marvisx-state-hook.py +182 -0
- core/api/config.py +533 -0
- core/api/db.py +1516 -0
- core/api/dependencies/__init__.py +0 -0
- core/api/dependencies/tenant.py +34 -0
- core/api/main.py +1641 -0
- core/api/mcp/__init__.py +8 -0
- core/api/mcp/_adapter.py +184 -0
- core/api/mcp/server.py +58 -0
- core/api/mcp/tools/__init__.py +59 -0
- core/api/mcp/tools/brain.py +599 -0
- core/api/mcp/tools/graph.py +380 -0
- core/api/mcp/tools/handoffs.py +112 -0
- core/api/mcp/tools/ingest.py +326 -0
- core/api/mcp/tools/learnings.py +144 -0
- core/api/mcp/tools/projects.py +99 -0
- core/api/mcp/tools/pull_requests.py +173 -0
- core/api/mcp/tools/safety.py +111 -0
- core/api/mcp/tools/search.py +79 -0
- core/api/mcp/tools/tasks.py +258 -0
- core/api/middleware/__init__.py +0 -0
- core/api/middleware/tool_call_audit.py +111 -0
- core/api/models/__init__.py +346 -0
- core/api/models/auth.py +48 -0
- core/api/models/brain.py +1006 -0
- core/api/models/common.py +76 -0
- core/api/models/costs.py +91 -0
- core/api/models/graph.py +66 -0
- core/api/models/graph_cosmo.py +125 -0
- core/api/models/graph_pr_impact.py +257 -0
- core/api/models/graph_ux.py +141 -0
- core/api/models/inbox.py +230 -0
- core/api/models/ingest_keys.py +108 -0
- core/api/models/kg.py +41 -0
- core/api/models/llm_config.py +56 -0
- core/api/models/monitoring.py +234 -0
- core/api/models/projects.py +161 -0
- core/api/models/search.py +42 -0
- core/api/models/sessions.py +322 -0
- core/api/models/tasks.py +184 -0
- core/api/models/teams.py +63 -0
- core/api/models/users.py +184 -0
- core/api/observability/__init__.py +0 -0
- core/api/observability/tracing.py +92 -0
- core/api/paths.py +26 -0
- core/api/rate_limit.py +24 -0
- core/api/rbac.py +112 -0
- core/api/routers/__init__.py +0 -0
- core/api/routers/_adapter.py +24 -0
- core/api/routers/admin_pr_impact.py +230 -0
- core/api/routers/admin_settings.py +147 -0
- core/api/routers/agent.py +1079 -0
- core/api/routers/agent_tokens.py +276 -0
- core/api/routers/app_settings.py +112 -0
- core/api/routers/audit.py +89 -0
- core/api/routers/auth.py +586 -0
- core/api/routers/bench.py +161 -0
- core/api/routers/brain.py +881 -0
- core/api/routers/brain_directions.py +527 -0
- core/api/routers/ci_checks.py +140 -0
- core/api/routers/comments.py +273 -0
- core/api/routers/costs.py +148 -0
- core/api/routers/docs_coverage.py +217 -0
- core/api/routers/docs_governance.py +63 -0
- core/api/routers/documents.py +318 -0
- core/api/routers/files.py +163 -0
- core/api/routers/finder.py +987 -0
- core/api/routers/graph.py +836 -0
- core/api/routers/handoffs.py +156 -0
- core/api/routers/inbox.py +496 -0
- core/api/routers/ingest_api_keys.py +205 -0
- core/api/routers/ingest_triage.py +1227 -0
- core/api/routers/judge.py +306 -0
- core/api/routers/kg.py +336 -0
- core/api/routers/learnings.py +253 -0
- core/api/routers/llm_config.py +130 -0
- core/api/routers/monitoring.py +347 -0
- core/api/routers/notifications.py +125 -0
- core/api/routers/pr_impact.py +315 -0
- core/api/routers/projects.py +1061 -0
- core/api/routers/pull_requests.py +312 -0
- core/api/routers/push.py +67 -0
- core/api/routers/raci.py +228 -0
- core/api/routers/search.py +125 -0
- core/api/routers/sessions.py +3100 -0
- core/api/routers/settings.py +90 -0
- core/api/routers/share_repo.py +68 -0
- core/api/routers/status_updates.py +96 -0
- core/api/routers/tags.py +45 -0
- core/api/routers/tasks.py +526 -0
- core/api/routers/teams.py +425 -0
- core/api/routers/terminal.py +105 -0
- core/api/routers/users.py +331 -0
- core/api/routers/webhooks.py +330 -0
- core/api/runtime_settings.py +84 -0
- core/api/security.py +652 -0
- core/api/services/__init__.py +0 -0
- core/api/services/audit.py +58 -0
- core/api/services/auto_approval.py +82 -0
- core/api/services/brain/__init__.py +52 -0
- core/api/services/brain/baseline.py +230 -0
- core/api/services/brain/capabilities.py +75 -0
- core/api/services/brain/cascade_rollup.py +388 -0
- core/api/services/brain/compound_bridge.py +215 -0
- core/api/services/brain/cycle.py +1242 -0
- core/api/services/brain/cycle_snapshot.py +371 -0
- core/api/services/brain/digest_collector.py +147 -0
- core/api/services/brain/direction.py +421 -0
- core/api/services/brain/drift.py +356 -0
- core/api/services/brain/drift_router.py +409 -0
- core/api/services/brain/edge_metrics.py +79 -0
- core/api/services/brain/events_reader.py +222 -0
- core/api/services/brain/findings.py +1379 -0
- core/api/services/brain/findings_reader.py +1006 -0
- core/api/services/brain/jobs.py +733 -0
- core/api/services/brain/journal.py +206 -0
- core/api/services/brain/knowledge_forms.py +92 -0
- core/api/services/brain/llm/__init__.py +37 -0
- core/api/services/brain/llm/_runner.py +62 -0
- core/api/services/brain/llm/base.py +70 -0
- core/api/services/brain/llm/cache.py +99 -0
- core/api/services/brain/llm/constants.py +46 -0
- core/api/services/brain/llm/direction_alignment.py +289 -0
- core/api/services/brain/llm/factory.py +132 -0
- core/api/services/brain/llm/finding_reasoning.py +98 -0
- core/api/services/brain/llm/finding_summary.py +92 -0
- core/api/services/brain/llm/grounding.py +46 -0
- core/api/services/brain/llm/journal_polish.py +96 -0
- core/api/services/brain/llm/local_gateway.py +426 -0
- core/api/services/brain/llm/parsers.py +71 -0
- core/api/services/brain/llm/router_glue.py +422 -0
- core/api/services/brain/memory_ops.py +1677 -0
- core/api/services/brain/models.py +140 -0
- core/api/services/brain/owner_hint.py +211 -0
- core/api/services/brain/recap.py +307 -0
- core/api/services/brain/rules/__init__.py +65 -0
- core/api/services/brain/rules/_signals.py +205 -0
- core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
- core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
- core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
- core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
- core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
- core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
- core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
- core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
- core/api/services/brain/runs_reader.py +485 -0
- core/api/services/brain/scope.py +79 -0
- core/api/services/brain/sources/__init__.py +46 -0
- core/api/services/brain/sources/base.py +86 -0
- core/api/services/brain/sources/git_kg.py +393 -0
- core/api/services/brain/sources/handoffs.py +157 -0
- core/api/services/brain/sources/ingestor.py +130 -0
- core/api/services/brain/sources/learnings.py +121 -0
- core/api/services/brain/sources/pir_tasks.py +245 -0
- core/api/services/brain/watermarks.py +147 -0
- core/api/services/brain/ws_emitter.py +170 -0
- core/api/services/cc_tasks_reader.py +76 -0
- core/api/services/ci_service.py +263 -0
- core/api/services/claude_metrics.py +796 -0
- core/api/services/codex_metrics.py +364 -0
- core/api/services/conversation_reader.py +102 -0
- core/api/services/cost_service.py +243 -0
- core/api/services/crypto.py +147 -0
- core/api/services/docs_governance/__init__.py +1 -0
- core/api/services/docs_governance/confidence.py +230 -0
- core/api/services/docs_governance/config.py +87 -0
- core/api/services/docs_governance/enrichment.py +65 -0
- core/api/services/docs_governance/frontmatter_validator.py +83 -0
- core/api/services/docs_governance/hard_gates.py +221 -0
- core/api/services/docs_governance/triage_orchestrator.py +98 -0
- core/api/services/embedding_internal.py +395 -0
- core/api/services/embedding_service.py +832 -0
- core/api/services/event_dispatcher.py +167 -0
- core/api/services/events.py +70 -0
- core/api/services/git_ops.py +621 -0
- core/api/services/graph_cosmo_service.py +440 -0
- core/api/services/graph_ranker.py +306 -0
- core/api/services/graph_service.py +1589 -0
- core/api/services/inbox.py +800 -0
- core/api/services/inbox_digest.py +221 -0
- core/api/services/inbox_digest_deep_research.py +80 -0
- core/api/services/inbox_digest_jobs.py +595 -0
- core/api/services/inbox_gmail_sync.py +167 -0
- core/api/services/inbox_llm_classifier.py +906 -0
- core/api/services/inbox_source_identity.py +116 -0
- core/api/services/inbox_sources.py +456 -0
- core/api/services/inbox_taxonomy.py +195 -0
- core/api/services/inbox_tldr.py +1079 -0
- core/api/services/inbox_triage.py +899 -0
- core/api/services/ingest/__init__.py +13 -0
- core/api/services/ingest/api_key_auth.py +136 -0
- core/api/services/ingest/auto_approve.py +120 -0
- core/api/services/ingest/classifier.py +138 -0
- core/api/services/ingest/confidence.py +173 -0
- core/api/services/ingest/dispatch.py +88 -0
- core/api/services/ingest/embedding_router.py +272 -0
- core/api/services/ingest/events.py +33 -0
- core/api/services/ingest/ignore_patterns.py +79 -0
- core/api/services/ingest/image_probe.py +218 -0
- core/api/services/ingest/ingress.py +263 -0
- core/api/services/ingest/insert_saga.py +793 -0
- core/api/services/ingest/llm/__init__.py +13 -0
- core/api/services/ingest/llm/anthropic_haiku.py +23 -0
- core/api/services/ingest/llm/base.py +59 -0
- core/api/services/ingest/llm/byok_provider.py +130 -0
- core/api/services/ingest/llm/classification_context.py +301 -0
- core/api/services/ingest/llm/config_store.py +246 -0
- core/api/services/ingest/llm/factory.py +24 -0
- core/api/services/ingest/llm/kg_enricher.py +306 -0
- core/api/services/ingest/llm/local_gateway.py +821 -0
- core/api/services/ingest/llm/local_vllm.py +23 -0
- core/api/services/ingest/llm/openai_nano.py +349 -0
- core/api/services/ingest/lock_advisory.py +57 -0
- core/api/services/ingest/parser_router.py +1756 -0
- core/api/services/ingest/parsers/__init__.py +1 -0
- core/api/services/ingest/parsers/docling_parser.py +142 -0
- core/api/services/ingest/parsers/docparse_gateway.py +178 -0
- core/api/services/ingest/parsers/docx_parser.py +127 -0
- core/api/services/ingest/parsers/folder_unpacker.py +85 -0
- core/api/services/ingest/parsers/gateway_aux.py +147 -0
- core/api/services/ingest/parsers/image_parser.py +251 -0
- core/api/services/ingest/parsers/internal_markdown.py +89 -0
- core/api/services/ingest/parsers/ocr_gateway.py +117 -0
- core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
- core/api/services/ingest/parsers/pdf_types.py +13 -0
- core/api/services/ingest/parsers/transcript_parser.py +445 -0
- core/api/services/ingest/parsers/vision_gateway.py +186 -0
- core/api/services/ingest/parsers/xlsx_parser.py +91 -0
- core/api/services/ingest/parsers/zip_unpacker.py +126 -0
- core/api/services/ingest/preflight.py +393 -0
- core/api/services/ingest/retry_voyage.py +88 -0
- core/api/services/ingest/routing_policy.py +307 -0
- core/api/services/ingest/serializers/__init__.py +1 -0
- core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
- core/api/services/ingest/skip_log.py +74 -0
- core/api/services/ingest/watcher.py +637 -0
- core/api/services/kg/__init__.py +0 -0
- core/api/services/kg/audit.py +49 -0
- core/api/services/kg/hybrid_search.py +691 -0
- core/api/services/kg/lens.py +339 -0
- core/api/services/kg/pr_impact.py +770 -0
- core/api/services/kg/queries.py +152 -0
- core/api/services/kg/ranking.py +89 -0
- core/api/services/kg/rrf.py +143 -0
- core/api/services/kg_watcher_control.py +161 -0
- core/api/services/local_llm/__init__.py +19 -0
- core/api/services/local_llm/async_client.py +385 -0
- core/api/services/local_llm/client.py +173 -0
- core/api/services/local_llm/url_validator.py +44 -0
- core/api/services/metrics_collector.py +646 -0
- core/api/services/metrics_providers.py +65 -0
- core/api/services/model_registry.py +266 -0
- core/api/services/model_router.py +137 -0
- core/api/services/n8n_client.py +77 -0
- core/api/services/newsletter_llm_gateway.py +66 -0
- core/api/services/notification_service.py +134 -0
- core/api/services/openai_responses.py +55 -0
- core/api/services/opencode_metrics.py +375 -0
- core/api/services/opencode_sessions.py +173 -0
- core/api/services/pii_redactor.py +138 -0
- core/api/services/pr_impact_pipeline/__init__.py +21 -0
- core/api/services/pr_impact_pipeline/differ.py +421 -0
- core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
- core/api/services/pr_impact_pipeline/gc.py +93 -0
- core/api/services/pr_impact_pipeline/languages.py +192 -0
- core/api/services/pr_impact_pipeline/parser.py +178 -0
- core/api/services/pr_impact_pipeline/writer.py +394 -0
- core/api/services/pr_service.py +1393 -0
- core/api/services/project_paths.py +70 -0
- core/api/services/project_status_updates.py +265 -0
- core/api/services/providers.py +276 -0
- core/api/services/push_service.py +170 -0
- core/api/services/reminder_service.py +89 -0
- core/api/services/runas.py +41 -0
- core/api/services/salience_service.py +69 -0
- core/api/services/security_collector.py +281 -0
- core/api/services/session_catalog.py +385 -0
- core/api/services/session_metrics_service.py +301 -0
- core/api/services/session_ops.py +272 -0
- core/api/services/session_state.py +173 -0
- core/api/services/share_links.py +222 -0
- core/api/services/task_transitions.py +146 -0
- core/api/services/terminal_metrics.py +462 -0
- core/api/services/terminal_metrics_dump.py +203 -0
- core/api/services/tmux.py +1205 -0
- core/api/services/webhook_service.py +422 -0
- core/api/services/workspace_sync.py +164 -0
- core/api/templates/__init__.py +1 -0
- core/api/templates/markdown_share.py +164 -0
- core/api/terminal.py +1031 -0
- core/api/tests/__init__.py +0 -0
- core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
- core/api/tests/test_audit_permissions.py +133 -0
- core/api/tests/test_backfill_session_conversations.py +90 -0
- core/api/tests/test_backfill_working_seconds_msg.py +129 -0
- core/api/tests/test_claude_metrics.py +326 -0
- core/api/tests/test_codex_metrics.py +189 -0
- core/api/tests/test_finder_paths.py +74 -0
- core/api/tests/test_git_ops_merge.py +155 -0
- core/api/tests/test_learnings_check_search.py +81 -0
- core/api/tests/test_metrics_providers.py +133 -0
- core/api/tests/test_migration_087.py +164 -0
- core/api/tests/test_migration_088.py +94 -0
- core/api/tests/test_migration_089.py +116 -0
- core/api/tests/test_openai_responses.py +24 -0
- core/api/tests/test_opencode_metrics.py +740 -0
- core/api/tests/test_opencode_sessions.py +321 -0
- core/api/tests/test_pr_workflow_e2e.py +457 -0
- core/api/tests/test_projects_handoffs.py +31 -0
- core/api/tests/test_providers.py +138 -0
- core/api/tests/test_safety_bridge.py +347 -0
- core/api/tests/test_session_catalog.py +142 -0
- core/api/tests/test_session_conversations.py +512 -0
- core/api/tests/test_session_metrics_service.py +270 -0
- core/api/tests/test_session_resume_paths.py +548 -0
- core/api/tests/test_session_theme_mode_migration.py +56 -0
- core/api/tests/test_sessions_rbac.py +131 -0
- core/api/tests/test_share_edit.py +398 -0
- core/api/tests/test_share_repo.py +200 -0
- core/api/tests/test_terminal_session_manager.py +98 -0
- core/api/tests/test_terminal_upload.py +34 -0
- core/api/tests/test_tmux.py +272 -0
- core/api/tests/test_workspace_sync.py +186 -0
- core/api/tests/test_ws_ticket_in_memory.py +73 -0
- core/api/use_cases/__init__.py +11 -0
- core/api/use_cases/_context.py +89 -0
- core/api/use_cases/_errors.py +62 -0
- core/api/use_cases/_roles.py +16 -0
- core/api/use_cases/audit.py +171 -0
- core/api/use_cases/brain.py +1232 -0
- core/api/use_cases/costs.py +249 -0
- core/api/use_cases/graph.py +1153 -0
- core/api/use_cases/handoffs.py +506 -0
- core/api/use_cases/ingest_triage.py +1229 -0
- core/api/use_cases/learnings.py +538 -0
- core/api/use_cases/projects.py +705 -0
- core/api/use_cases/pull_requests.py +415 -0
- core/api/use_cases/search.py +926 -0
- core/api/use_cases/tasks.py +1495 -0
- core/api/visibility.py +141 -0
- core/cli/__init__.py +5 -0
- core/cli/_index_source.py +632 -0
- core/cli/_runtime_ctx.py +160 -0
- core/cli/_transmute.py +241 -0
- core/cli/marvis_doctor.py +704 -0
- core/cli/marvis_feedback.py +396 -0
- core/cli/marvis_governance.py +315 -0
- core/cli/marvis_hooks.py +515 -0
- core/cli/marvis_init.py +757 -0
- core/cli/marvis_mcp.py +401 -0
- core/cli/marvis_runtime.py +855 -0
- core/cli/marvis_telemetry.py +228 -0
- core/scripts/_drift_check.py +716 -0
- core/scripts/_frontmatter.py +66 -0
- core/scripts/_graph_writer.py +189 -0
- core/scripts/ast_parser.py +1553 -0
- core/scripts/install_hooks/__init__.py +1 -0
- core/scripts/install_hooks/_config.sh +109 -0
- core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
- core/scripts/install_hooks/block-db-direct-write.sh +23 -0
- core/scripts/install_hooks/block-push-no-task.sh +23 -0
- core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
- core/scripts/install_hooks/block-subtree-push.sh +23 -0
- core/scripts/install_hooks/config.json +53 -0
- core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
- core/scripts/install_hooks/enforce-worktree.sh +23 -0
- core/scripts/install_hooks/quality-gate.sh +170 -0
- core/scripts/install_hooks/safety_bridge.py +968 -0
- core/scripts/install_hooks/secret-scan.sh +23 -0
- core/scripts/migrate_spike_node_ids.py +122 -0
- core/scripts/populate_artifacts.py +2198 -0
- core/scripts/populate_cross_project.py +2457 -0
- core/scripts/populate_inbox_nodes.py +357 -0
- core/scripts/populate_pr_impact.py +267 -0
- core/scripts/populate_project_nodes.py +603 -0
- core/scripts/populate_touch_counter.py +337 -0
- core/scripts/reparse_failed.py +57 -0
- core/scripts/safety_bridge.py +968 -0
- core/telemetry/__init__.py +9 -0
- core/telemetry/client.py +405 -0
- core/telemetry/schema.py +122 -0
- core/wizard/__init__.py +65 -0
- core/wizard/byok_vault.py +147 -0
- core/wizard/defaults.py +58 -0
- core/wizard/state.py +117 -0
- core/wizard/steps.py +70 -0
- core/wizard/validation.py +136 -0
- marvisx_cli-0.1.0.dist-info/METADATA +201 -0
- marvisx_cli-0.1.0.dist-info/RECORD +587 -0
- marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
- marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
- marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
- migrations/001_initial.sql +33 -0
- migrations/002_tasks.sql +30 -0
- migrations/003_session_management.sql +7 -0
- migrations/004_projects_comments.sql +65 -0
- migrations/005_session_intelligence.sql +15 -0
- migrations/006_settings.sql +12 -0
- migrations/007_task_scoring.sql +12 -0
- migrations/008_cost_tracking.sql +31 -0
- migrations/009_session_card_metrics.sql +3 -0
- migrations/010_monitoring.sql +55 -0
- migrations/012_agent_api.sql +21 -0
- migrations/013_session_complete.sql +8 -0
- migrations/015_pull_requests.sql +43 -0
- migrations/015_pull_requests_down.sql +5 -0
- migrations/016_users_raci.sql +116 -0
- migrations/017_task_cost_entries.sql +87 -0
- migrations/018_agents.sql +73 -0
- migrations/018_agents_down.sql +13 -0
- migrations/019_review_feedback.sql +18 -0
- migrations/020_pr_commit_sha.sql +4 -0
- migrations/021_webhook_events.sql +18 -0
- migrations/022_devx_agent_managed.sql +11 -0
- migrations/022_devx_agent_managed_down.sql +6 -0
- migrations/023_devx_p1_gate.sql +7 -0
- migrations/023_devx_p1_gate_down.sql +3 -0
- migrations/024_chat_messages.sql +16 -0
- migrations/024_pr_conversation_id.sql +8 -0
- migrations/024_task_indexes.sql +21 -0
- migrations/024_task_indexes_down.sql +7 -0
- migrations/025_audit_log.sql +17 -0
- migrations/026_agent_tokens.sql +20 -0
- migrations/027_teams_auth_phase_b.sql +35 -0
- migrations/028_learnings.sql +23 -0
- migrations/029_team_roles.sql +14 -0
- migrations/030_finder_pins.sql +10 -0
- migrations/031_pr_deploy_status.sql +9 -0
- migrations/032_task_reminders.sql +7 -0
- migrations/033_events_retry_count.sql +6 -0
- migrations/033_session_owner.sql +9 -0
- migrations/034_notifications.sql +38 -0
- migrations/035_shared_links.sql +15 -0
- migrations/036_session_index_upgrade.sql +29 -0
- migrations/037_pr_approval.sql +15 -0
- migrations/038_pr_submitted_by.sql +6 -0
- migrations/039_push_subscriptions.sql +17 -0
- migrations/040_semantic_search.sql +16 -0
- migrations/041_workspaces.sql +63 -0
- migrations/042_oidc_providers.sql +24 -0
- migrations/043_ci_checks.sql +31 -0
- migrations/044_agent_metrics.sql +30 -0
- migrations/045_documents_doc_type.sql +5 -0
- migrations/046_salience.sql +13 -0
- migrations/047_seed_missing_agents.sql +6 -0
- migrations/048_fix_agent_paths_roles.sql +5 -0
- migrations/049_agent_role_and_learnings_schema.sql +3 -0
- migrations/050_session_provider.sql +2 -0
- migrations/051_session_launch_profile.sql +4 -0
- migrations/052_session_theme_mode.sql +2 -0
- migrations/052_task_kind.sql +4 -0
- migrations/053_inbox_items.sql +31 -0
- migrations/054_inbox_triage_contract.sql +30 -0
- migrations/055_inbox_topic_treatment.sql +12 -0
- migrations/056_inbox_treatment_read_save.sql +57 -0
- migrations/057_session_theme_mode_backfill.sql +4 -0
- migrations/058_inbox_item_status_lifecycle.sql +13 -0
- migrations/059_inbox_tldr_and_source_scores.sql +18 -0
- migrations/060_newsletter.sql +16 -0
- migrations/061_inbox_redesign.sql +69 -0
- migrations/062_fix_inbox_sources_backfill.sql +37 -0
- migrations/063_task_completion_mode.sql +23 -0
- migrations/064_judge_mode_setting.sql +4 -0
- migrations/065_knowledge_graph_spike.sql +40 -0
- migrations/066_digest_ranking_inputs.sql +10 -0
- migrations/066_kg_artifact_nodes.sql +129 -0
- migrations/067_inbox_digest_selections.sql +28 -0
- migrations/067_kg_temporal.sql +53 -0
- migrations/068_inbox_digest_app_settings.sql +9 -0
- migrations/068_kg_touch_counter.sql +52 -0
- migrations/069_kg_doc_types.sql +117 -0
- migrations/070_digest_ranking_inputs_recovery.sql +3 -0
- migrations/071_inbox_digest_selections_recovery.sql +3 -0
- migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
- migrations/073_kg_cross_project.sql +216 -0
- migrations/073_kg_cross_project_down.sql +77 -0
- migrations/074_kg_infra_types.sql +208 -0
- migrations/074_kg_infra_types_down.sql +80 -0
- migrations/075_kg_file_state_recovery.sql +35 -0
- migrations/075_kg_file_state_recovery_down.sql +5 -0
- migrations/076_kg_watcher_state.sql +33 -0
- migrations/076_kg_watcher_state_down.sql +3 -0
- migrations/077_kg_doc_types_extend.sql +226 -0
- migrations/077_kg_doc_types_extend_down.sql +80 -0
- migrations/078_kg_fts5.sql +102 -0
- migrations/078_kg_fts5_down.sql +14 -0
- migrations/079_kg_missing_indexes.sql +31 -0
- migrations/079_kg_missing_indexes_down.sql +10 -0
- migrations/080_kg_fts5_extended.sql +232 -0
- migrations/080_kg_fts5_extended_down.sql +25 -0
- migrations/081_kg_lens_indexes.sql +9 -0
- migrations/081_kg_lens_indexes_down.sql +3 -0
- migrations/082_kg_pins.sql +26 -0
- migrations/082_kg_pins_down.sql +14 -0
- migrations/083_kg_graph_nodes_degree.sql +20 -0
- migrations/083_kg_graph_nodes_degree_down.sql +15 -0
- migrations/084_drop_legacy_scheduler_tables.sql +58 -0
- migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
- migrations/085_kg_edge_resolves_to.sql +142 -0
- migrations/085_kg_edge_resolves_to_down.sql +66 -0
- migrations/086_project_status_updates_feed.sql +20 -0
- migrations/086_project_status_updates_feed_down.sql +36 -0
- migrations/087_session_metrics_dual.sql +50 -0
- migrations/087_session_metrics_dual_down.sql +21 -0
- migrations/088_rename_context_pct_legacy.sql +23 -0
- migrations/088_rename_context_pct_legacy_down.sql +8 -0
- migrations/089_session_metrics_equivalent_cost.sql +26 -0
- migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
- migrations/090_kg_inbox_node_type.sql +26 -0
- migrations/090_kg_inbox_node_type_down.sql +20 -0
- migrations/091_kg_inbox_node_type_check.sql +265 -0
- migrations/091_kg_inbox_node_type_check_down.sql +129 -0
- migrations/092_sessions_activity_state_ts.sql +29 -0
- migrations/092_sessions_activity_state_ts_down.sql +14 -0
- migrations/093_sessions_activity_state_column.sql +29 -0
- migrations/093_sessions_activity_state_column_down.sql +10 -0
- migrations/094_ingest_pending.sql +55 -0
- migrations/094_ingest_pending_down.sql +15 -0
- migrations/095_kg_intent_first.sql +77 -0
- migrations/095_kg_intent_first_down.sql +25 -0
- migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
- migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
- migrations/097_ingest_change_history.sql +37 -0
- migrations/097_ingest_change_history_down.sql +13 -0
- migrations/098_kg_node_type_business.sql +254 -0
- migrations/098_kg_node_type_business_down.sql +195 -0
- migrations/099_kg_edges_restore_weight.sql +58 -0
- migrations/099_kg_edges_restore_weight_down.sql +12 -0
- migrations/100_kg_enriched_at.sql +25 -0
- migrations/100_kg_enriched_at_down.sql +12 -0
- migrations/101_local_llm_shadow_comparisons.sql +66 -0
- migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
- migrations/102_promote_llm_costs.sql +69 -0
- migrations/102_promote_llm_costs_down.sql +19 -0
- migrations/103_ingest_skipped_log.sql +46 -0
- migrations/103_ingest_skipped_log_down.sql +15 -0
- migrations/120_docs_governance.sql +50 -0
- migrations/120_docs_governance_down.sql +11 -0
- migrations/121_notification_event_fk_cleanup.sql +21 -0
- migrations/121_notification_event_fk_cleanup_down.sql +10 -0
- migrations/122_docs_drift_history.sql +34 -0
- migrations/122_docs_drift_history_down.sql +15 -0
- migrations/123_ingest_parser_waiting_status.sql +69 -0
- migrations/123_ingest_parser_waiting_status_down.sql +69 -0
- migrations/124_heypocket_recordings.sql +63 -0
- migrations/124_heypocket_recordings_down.sql +13 -0
- migrations/125_kg_node_type_record.sql +219 -0
- migrations/125_kg_node_type_record_down.sql +205 -0
- migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
- migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
- migrations/127_brain_v1_substrate.sql +200 -0
- migrations/127_brain_v1_substrate_down.sql +32 -0
- migrations/128_brain_drift_signals.sql +157 -0
- migrations/128_brain_drift_signals_down.sql +23 -0
- migrations/129_brain_memory_operations.sql +232 -0
- migrations/129_brain_memory_operations_down.sql +27 -0
- migrations/130_brain_findings.sql +258 -0
- migrations/130_brain_findings_down.sql +29 -0
- migrations/132_kg_pr_modifies.sql +242 -0
- migrations/132_kg_pr_modifies_down.sql +99 -0
- migrations/133_brain_v1_2_direction_schema.sql +476 -0
- migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
- migrations/134_brain_journal_narrative_polished.sql +8 -0
- migrations/134_brain_journal_narrative_polished_down.sql +6 -0
- migrations/135_kg_edges_provider.sql +21 -0
- migrations/136_documents_fts.sql +56 -0
- migrations/137_promote_llm_costs.sql +59 -0
- migrations/137_promote_llm_costs_down.sql +19 -0
- migrations/138_ingest_api_keys.sql +39 -0
- migrations/138_ingest_api_keys_down.sql +8 -0
- migrations/139_ingest_pending_ingress.sql +91 -0
- migrations/139_ingest_pending_ingress_down.sql +73 -0
- migrations/140_ingest_idempotency_quota.sql +45 -0
- migrations/140_ingest_idempotency_quota_down.sql +9 -0
- migrations/141_ingest_pending_metadata.sql +16 -0
- migrations/141_ingest_pending_metadata_down.sql +7 -0
- migrations/142_llm_function_config.sql +36 -0
- migrations/142_llm_function_config_down.sql +8 -0
- migrations/143_kg_code_embeddings.sql +25 -0
- migrations/143_kg_code_embeddings_down.sql +5 -0
- migrations/__init__.py +4 -0
- projects/_template/project.yaml +46 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Brain v1 — Runs read API (sub-05 §2 + sub-01 §6.D4).
|
|
2
|
+
# Pulls envelope rows from brain_runs and projects them to the BrainRun
|
|
3
|
+
# Pydantic contract. Cursor pagination + visibility-passthrough.
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import aiosqlite
|
|
12
|
+
|
|
13
|
+
from core.api.db import acquire_db, write_db
|
|
14
|
+
from core.api.models.brain import BrainRun, PipelineCounters, RunsListResponse, RunStatus
|
|
15
|
+
|
|
16
|
+
DEFAULT_LIMIT = 50
|
|
17
|
+
MAX_LIMIT = 200
|
|
18
|
+
|
|
19
|
+
_VALID_STATUSES: tuple[str, ...] = (
|
|
20
|
+
"running",
|
|
21
|
+
"succeeded",
|
|
22
|
+
"partial",
|
|
23
|
+
"failed",
|
|
24
|
+
"superseded",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_iso(value: str | None) -> datetime | None:
|
|
29
|
+
if value is None:
|
|
30
|
+
return None
|
|
31
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
32
|
+
if dt.tzinfo is None:
|
|
33
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
34
|
+
return dt
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _encode_cursor(cycle_key: str, run_id: str) -> str:
|
|
38
|
+
payload = json.dumps({"c": cycle_key, "r": run_id}, separators=(",", ":"))
|
|
39
|
+
return base64.urlsafe_b64encode(payload.encode("utf-8")).decode("ascii")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _decode_cursor(cursor: str | None) -> tuple[str, str] | None:
|
|
43
|
+
if not cursor:
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
raw = base64.urlsafe_b64decode(cursor.encode("ascii")).decode("utf-8")
|
|
47
|
+
parsed = json.loads(raw)
|
|
48
|
+
return str(parsed["c"]), str(parsed["r"])
|
|
49
|
+
except (ValueError, KeyError, json.JSONDecodeError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _row_to_run(row: aiosqlite.Row) -> BrainRun:
|
|
54
|
+
failures_raw = row["partial_failures_json"] if "partial_failures_json" in row.keys() else "[]"
|
|
55
|
+
try:
|
|
56
|
+
partial = json.loads(failures_raw or "[]")
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
partial = []
|
|
59
|
+
return BrainRun(
|
|
60
|
+
run_id=row["run_id"],
|
|
61
|
+
workspace_id=row["workspace_id"],
|
|
62
|
+
cycle_key=row["cycle_key"],
|
|
63
|
+
cycle_window_start_utc=_parse_iso(row["cycle_window_start_utc"]),
|
|
64
|
+
cycle_window_end_utc=_parse_iso(row["cycle_window_end_utc"]),
|
|
65
|
+
cutoff_hour_utc_at_run=int(row["cutoff_hour_utc_at_run"] or 0),
|
|
66
|
+
scope_type="company",
|
|
67
|
+
scope_key=row["scope_key"] or "__company__",
|
|
68
|
+
trigger=row["trigger"],
|
|
69
|
+
triggered_by=row["triggered_by"],
|
|
70
|
+
started_at=_parse_iso(row["started_at"]),
|
|
71
|
+
finished_at=_parse_iso(row["finished_at"]),
|
|
72
|
+
status=row["status"],
|
|
73
|
+
superseded_by_run_id=row["superseded_by_run_id"],
|
|
74
|
+
event_count=int(row["event_count"] or 0),
|
|
75
|
+
partial_failures=partial,
|
|
76
|
+
duration_ms=row["duration_ms"],
|
|
77
|
+
error_summary=row["error_summary"],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _resolve_latest_cycle_key(
|
|
82
|
+
db: aiosqlite.Connection, *, workspace_id: str
|
|
83
|
+
) -> str | None:
|
|
84
|
+
async with db.execute(
|
|
85
|
+
"SELECT cycle_key FROM brain_runs "
|
|
86
|
+
"WHERE workspace_id = ? AND status = 'succeeded' "
|
|
87
|
+
"AND superseded_by_run_id IS NULL "
|
|
88
|
+
"ORDER BY cycle_key DESC LIMIT 1",
|
|
89
|
+
(workspace_id,),
|
|
90
|
+
) as cur:
|
|
91
|
+
row = await cur.fetchone()
|
|
92
|
+
return row["cycle_key"] if row else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def list_runs(
|
|
96
|
+
*,
|
|
97
|
+
cycle_key: str | None,
|
|
98
|
+
status: list[str] | None,
|
|
99
|
+
trigger: list[str] | None,
|
|
100
|
+
include_superseded: bool,
|
|
101
|
+
cursor: str | None,
|
|
102
|
+
limit: int,
|
|
103
|
+
workspace_id: str = "ws_default",
|
|
104
|
+
) -> RunsListResponse:
|
|
105
|
+
"""Stable sort: (cycle_key DESC, run_id ASC)."""
|
|
106
|
+
where = ["workspace_id = ?"]
|
|
107
|
+
params: list[Any] = [workspace_id]
|
|
108
|
+
resolved_cycle: str | None = None
|
|
109
|
+
|
|
110
|
+
if cycle_key == "latest":
|
|
111
|
+
async with acquire_db() as db:
|
|
112
|
+
db.row_factory = aiosqlite.Row
|
|
113
|
+
resolved_cycle = await _resolve_latest_cycle_key(
|
|
114
|
+
db, workspace_id=workspace_id
|
|
115
|
+
)
|
|
116
|
+
if resolved_cycle:
|
|
117
|
+
where.append("cycle_key = ?")
|
|
118
|
+
params.append(resolved_cycle)
|
|
119
|
+
else:
|
|
120
|
+
return RunsListResponse(
|
|
121
|
+
items=[], next_cursor=None, cycle_key=None, total_returned=0
|
|
122
|
+
)
|
|
123
|
+
elif cycle_key:
|
|
124
|
+
where.append("cycle_key = ?")
|
|
125
|
+
params.append(cycle_key)
|
|
126
|
+
resolved_cycle = cycle_key
|
|
127
|
+
|
|
128
|
+
if status:
|
|
129
|
+
valid = [s for s in status if s in _VALID_STATUSES]
|
|
130
|
+
if valid:
|
|
131
|
+
placeholders = ",".join("?" for _ in valid)
|
|
132
|
+
where.append(f"status IN ({placeholders})")
|
|
133
|
+
params.extend(valid)
|
|
134
|
+
|
|
135
|
+
if trigger:
|
|
136
|
+
placeholders = ",".join("?" for _ in trigger)
|
|
137
|
+
where.append(f"trigger IN ({placeholders})")
|
|
138
|
+
params.extend(trigger)
|
|
139
|
+
|
|
140
|
+
if not include_superseded:
|
|
141
|
+
where.append("superseded_by_run_id IS NULL")
|
|
142
|
+
|
|
143
|
+
decoded = _decode_cursor(cursor)
|
|
144
|
+
if decoded:
|
|
145
|
+
cur_cycle, cur_run = decoded
|
|
146
|
+
where.append("(cycle_key < ? OR (cycle_key = ? AND run_id > ?))")
|
|
147
|
+
params.extend([cur_cycle, cur_cycle, cur_run])
|
|
148
|
+
|
|
149
|
+
fetch_limit = min(max(limit, 1), MAX_LIMIT) + 1
|
|
150
|
+
query = (
|
|
151
|
+
"SELECT run_id, workspace_id, cycle_key, "
|
|
152
|
+
"cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
|
|
153
|
+
"scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
|
|
154
|
+
"status, superseded_by_run_id, event_count, partial_failures_json, "
|
|
155
|
+
"duration_ms, error_summary "
|
|
156
|
+
"FROM brain_runs WHERE " + " AND ".join(where) +
|
|
157
|
+
" ORDER BY cycle_key DESC, run_id ASC LIMIT ?"
|
|
158
|
+
)
|
|
159
|
+
params.append(fetch_limit)
|
|
160
|
+
|
|
161
|
+
async with acquire_db() as db:
|
|
162
|
+
db.row_factory = aiosqlite.Row
|
|
163
|
+
rows = list(await (await db.execute(query, params)).fetchall())
|
|
164
|
+
|
|
165
|
+
next_cursor: str | None = None
|
|
166
|
+
if len(rows) > limit:
|
|
167
|
+
last = rows[limit - 1]
|
|
168
|
+
next_cursor = _encode_cursor(last["cycle_key"], last["run_id"])
|
|
169
|
+
rows = rows[:limit]
|
|
170
|
+
|
|
171
|
+
items = [_row_to_run(r) for r in rows]
|
|
172
|
+
return RunsListResponse(
|
|
173
|
+
items=items,
|
|
174
|
+
next_cursor=next_cursor,
|
|
175
|
+
cycle_key=resolved_cycle,
|
|
176
|
+
total_returned=len(items),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PromoteError(Exception):
|
|
181
|
+
"""Raised when a promote_run pre-condition fails (404 / 422 surface)."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, kind: str, detail: str):
|
|
184
|
+
self.kind = kind
|
|
185
|
+
self.detail = detail
|
|
186
|
+
super().__init__(detail)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def promote_run(
|
|
190
|
+
run_id: str, *, workspace_id: str = "ws_default"
|
|
191
|
+
) -> BrainRun:
|
|
192
|
+
"""Revert a supersede so a previously-replaced run becomes UX-visible again.
|
|
193
|
+
|
|
194
|
+
Use case: a low-yield manual recompute superseded a richer auto-cron run.
|
|
195
|
+
The richer run still holds its journal/drift/memory_op/finding rows but
|
|
196
|
+
`superseded_by_run_id` keeps them hidden behind the visibility filter
|
|
197
|
+
(`r.superseded_by_run_id IS NULL`) on every read path.
|
|
198
|
+
|
|
199
|
+
Effects:
|
|
200
|
+
1. Validate target run exists in workspace AND is currently superseded.
|
|
201
|
+
2. Clear `superseded_by_run_id` on target → it surfaces again.
|
|
202
|
+
3. Cascade-clear: the successor that pointed at target loses its
|
|
203
|
+
`superseded_by_run_id` reverse-pointer entry only if it itself was
|
|
204
|
+
the chain head (no further superseder). This keeps the chain
|
|
205
|
+
well-formed: at most one head per cycle is UX-visible.
|
|
206
|
+
|
|
207
|
+
Raises `PromoteError` with kind:
|
|
208
|
+
- 'not_found': run_id unknown in workspace.
|
|
209
|
+
- 'not_superseded': target run is already UX-visible (idempotency).
|
|
210
|
+
"""
|
|
211
|
+
# Read pre-conditions on the readonly pool (fast path; no writer lock).
|
|
212
|
+
async with acquire_db() as db:
|
|
213
|
+
db.row_factory = aiosqlite.Row
|
|
214
|
+
async with db.execute(
|
|
215
|
+
"SELECT run_id, status, superseded_by_run_id, cycle_key "
|
|
216
|
+
"FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
|
|
217
|
+
(run_id, workspace_id),
|
|
218
|
+
) as cur:
|
|
219
|
+
target = await cur.fetchone()
|
|
220
|
+
if target is None:
|
|
221
|
+
raise PromoteError("not_found", f"run {run_id} not found")
|
|
222
|
+
successor_run_id = target["superseded_by_run_id"]
|
|
223
|
+
if successor_run_id is None:
|
|
224
|
+
raise PromoteError(
|
|
225
|
+
"not_superseded",
|
|
226
|
+
f"run {run_id} is already the visible head (no superseder to revert)",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# SWAP heads on the writer connection (single-writer lock).
|
|
230
|
+
# Order matters: the partial unique index
|
|
231
|
+
# uniq_brain_runs_active_cycle ON (workspace_id, cycle_key)
|
|
232
|
+
# WHERE status IN ('running','succeeded') AND superseded_by_run_id IS NULL
|
|
233
|
+
# caps the cycle at one "head" row. If we mark target as head BEFORE
|
|
234
|
+
# demoting successor, both rows transiently match the index → IntegrityError.
|
|
235
|
+
# Demote successor FIRST (it drops out of the index because status moves
|
|
236
|
+
# to 'superseded'), THEN promote target (it joins the index in successor's
|
|
237
|
+
# place). At most one head per cycle at any point during the txn.
|
|
238
|
+
async with write_db() as db:
|
|
239
|
+
db.row_factory = aiosqlite.Row
|
|
240
|
+
# Step 1 — demote successor: status -> 'superseded' + point at target.
|
|
241
|
+
await db.execute(
|
|
242
|
+
"UPDATE brain_runs SET superseded_by_run_id = ?, "
|
|
243
|
+
"status = CASE WHEN status = 'succeeded' THEN 'superseded' ELSE status END "
|
|
244
|
+
"WHERE run_id = ? AND workspace_id = ?",
|
|
245
|
+
(run_id, successor_run_id, workspace_id),
|
|
246
|
+
)
|
|
247
|
+
# Step 2 — promote target: clear pointer + status -> 'succeeded'.
|
|
248
|
+
await db.execute(
|
|
249
|
+
"UPDATE brain_runs SET superseded_by_run_id = NULL, "
|
|
250
|
+
"status = CASE WHEN status = 'superseded' THEN 'succeeded' ELSE status END "
|
|
251
|
+
"WHERE run_id = ? AND workspace_id = ?",
|
|
252
|
+
(run_id, workspace_id),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Re-read the promoted run for response inside the same writer txn
|
|
256
|
+
# so we observe the post-update state.
|
|
257
|
+
async with db.execute(
|
|
258
|
+
"SELECT run_id, workspace_id, cycle_key, "
|
|
259
|
+
"cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
|
|
260
|
+
"scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
|
|
261
|
+
"status, superseded_by_run_id, event_count, partial_failures_json, "
|
|
262
|
+
"duration_ms, error_summary "
|
|
263
|
+
"FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
|
|
264
|
+
(run_id, workspace_id),
|
|
265
|
+
) as cur:
|
|
266
|
+
row = await cur.fetchone()
|
|
267
|
+
return _row_to_run(row)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class DiscardError(Exception):
|
|
271
|
+
"""Raised when a discard_run pre-condition fails (404 / 422 surface)."""
|
|
272
|
+
|
|
273
|
+
def __init__(self, kind: str, detail: str):
|
|
274
|
+
self.kind = kind
|
|
275
|
+
self.detail = detail
|
|
276
|
+
super().__init__(detail)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def discard_run(
|
|
280
|
+
run_id: str, *, workspace_id: str = "ws_default"
|
|
281
|
+
) -> BrainRun:
|
|
282
|
+
"""Mark a head run as 'failed' so it drops out of UX `latest` resolution.
|
|
283
|
+
|
|
284
|
+
Use case: a low-yield manual recompute creates a head run (e.g. cycle
|
|
285
|
+
forward 2026-05-18 with 1 event) that intercepts `cycle_key=latest`
|
|
286
|
+
resolution. Discarding the head removes it from the active-cycle
|
|
287
|
+
partial unique index (which only matches `status IN
|
|
288
|
+
('running','succeeded') AND superseded_by_run_id IS NULL`), so the
|
|
289
|
+
next visible cycle (the richer one one day prior) wins `latest`.
|
|
290
|
+
|
|
291
|
+
Effects:
|
|
292
|
+
1. Validate target run exists, is the head (superseded_by_run_id NULL),
|
|
293
|
+
and is currently in a discardable status (succeeded/partial).
|
|
294
|
+
2. Set status = 'failed' (preserves audit trail vs. delete; row stays
|
|
295
|
+
queryable with `include_superseded=true` for forensics).
|
|
296
|
+
|
|
297
|
+
Raises `DiscardError` with kind:
|
|
298
|
+
- 'not_found': run_id unknown in workspace.
|
|
299
|
+
- 'not_head': run is already superseded (won't surface anyway).
|
|
300
|
+
- 'not_discardable': status is already terminal-non-success.
|
|
301
|
+
"""
|
|
302
|
+
async with acquire_db() as db:
|
|
303
|
+
db.row_factory = aiosqlite.Row
|
|
304
|
+
async with db.execute(
|
|
305
|
+
"SELECT run_id, status, superseded_by_run_id, cycle_key "
|
|
306
|
+
"FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
|
|
307
|
+
(run_id, workspace_id),
|
|
308
|
+
) as cur:
|
|
309
|
+
target = await cur.fetchone()
|
|
310
|
+
if target is None:
|
|
311
|
+
raise DiscardError("not_found", f"run {run_id} not found")
|
|
312
|
+
if target["superseded_by_run_id"] is not None:
|
|
313
|
+
raise DiscardError(
|
|
314
|
+
"not_head",
|
|
315
|
+
f"run {run_id} is already superseded — won't surface in UX latest",
|
|
316
|
+
)
|
|
317
|
+
if target["status"] not in ("succeeded", "partial"):
|
|
318
|
+
raise DiscardError(
|
|
319
|
+
"not_discardable",
|
|
320
|
+
f"run {run_id} has status='{target['status']}' (only succeeded/partial discardable)",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async with write_db() as db:
|
|
324
|
+
db.row_factory = aiosqlite.Row
|
|
325
|
+
await db.execute(
|
|
326
|
+
"UPDATE brain_runs SET status = 'failed' "
|
|
327
|
+
"WHERE run_id = ? AND workspace_id = ?",
|
|
328
|
+
(run_id, workspace_id),
|
|
329
|
+
)
|
|
330
|
+
async with db.execute(
|
|
331
|
+
"SELECT run_id, workspace_id, cycle_key, "
|
|
332
|
+
"cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
|
|
333
|
+
"scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
|
|
334
|
+
"status, superseded_by_run_id, event_count, partial_failures_json, "
|
|
335
|
+
"duration_ms, error_summary "
|
|
336
|
+
"FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
|
|
337
|
+
(run_id, workspace_id),
|
|
338
|
+
) as cur:
|
|
339
|
+
row = await cur.fetchone()
|
|
340
|
+
return _row_to_run(row)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def fetch_single_run(
|
|
344
|
+
run_id: str, *, workspace_id: str = "ws_default"
|
|
345
|
+
) -> BrainRun | None:
|
|
346
|
+
async with acquire_db() as db:
|
|
347
|
+
db.row_factory = aiosqlite.Row
|
|
348
|
+
async with db.execute(
|
|
349
|
+
"SELECT run_id, workspace_id, cycle_key, "
|
|
350
|
+
"cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
|
|
351
|
+
"scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
|
|
352
|
+
"status, superseded_by_run_id, event_count, partial_failures_json, "
|
|
353
|
+
"duration_ms, error_summary "
|
|
354
|
+
"FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
|
|
355
|
+
(run_id, workspace_id),
|
|
356
|
+
) as cur:
|
|
357
|
+
row = await cur.fetchone()
|
|
358
|
+
return _row_to_run(row) if row else None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def get_pipeline_counters(
|
|
362
|
+
*, cycle_key: str | None, workspace_id: str = "ws_default"
|
|
363
|
+
) -> PipelineCounters:
|
|
364
|
+
"""Aggregate 6-station counts for the PipelineSubbar.
|
|
365
|
+
|
|
366
|
+
Resolves `latest` server-side. Counts are unfiltered (workspace-scoped) —
|
|
367
|
+
visibility is applied client-side via redacted_count on downstream calls.
|
|
368
|
+
The subbar is a status indicator, not a privileged surface.
|
|
369
|
+
"""
|
|
370
|
+
async with acquire_db() as db:
|
|
371
|
+
db.row_factory = aiosqlite.Row
|
|
372
|
+
|
|
373
|
+
resolved_cycle = cycle_key
|
|
374
|
+
if cycle_key == "latest" or cycle_key is None:
|
|
375
|
+
resolved_cycle = await _resolve_latest_cycle_key(
|
|
376
|
+
db, workspace_id=workspace_id
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if not resolved_cycle:
|
|
380
|
+
return PipelineCounters(
|
|
381
|
+
cycle_key=cycle_key or "latest",
|
|
382
|
+
run_id=None,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
async with db.execute(
|
|
386
|
+
"SELECT run_id, event_count FROM brain_runs "
|
|
387
|
+
"WHERE workspace_id = ? AND cycle_key = ? "
|
|
388
|
+
"AND status IN ('succeeded', 'partial') "
|
|
389
|
+
"AND superseded_by_run_id IS NULL "
|
|
390
|
+
"ORDER BY started_at DESC LIMIT 1",
|
|
391
|
+
(workspace_id, resolved_cycle),
|
|
392
|
+
) as cur:
|
|
393
|
+
run_row = await cur.fetchone()
|
|
394
|
+
|
|
395
|
+
run_id = run_row["run_id"] if run_row else None
|
|
396
|
+
digest_count = int(run_row["event_count"]) if run_row else 0
|
|
397
|
+
|
|
398
|
+
# Journal: count entries for this run
|
|
399
|
+
journal_count = 0
|
|
400
|
+
if run_id:
|
|
401
|
+
async with db.execute(
|
|
402
|
+
"SELECT COUNT(*) AS c FROM brain_journal_entries "
|
|
403
|
+
"WHERE workspace_id = ? AND run_id = ?",
|
|
404
|
+
(workspace_id, run_id),
|
|
405
|
+
) as cur:
|
|
406
|
+
row = await cur.fetchone()
|
|
407
|
+
journal_count = int(row["c"]) if row else 0
|
|
408
|
+
|
|
409
|
+
# Drift: count signals in 'open' state for this cycle.
|
|
410
|
+
# brain_drift_signals does NOT have a workspace_id column — visibility
|
|
411
|
+
# is enforced on read paths via join to brain_runs. For the counter
|
|
412
|
+
# (status indicator, not privileged surface — see docstring above)
|
|
413
|
+
# cycle_key alone is sufficient.
|
|
414
|
+
drift_count = 0
|
|
415
|
+
try:
|
|
416
|
+
async with db.execute(
|
|
417
|
+
"SELECT COUNT(*) AS c FROM brain_drift_signals "
|
|
418
|
+
"WHERE cycle_key = ? AND state = 'open'",
|
|
419
|
+
(resolved_cycle,),
|
|
420
|
+
) as cur:
|
|
421
|
+
row = await cur.fetchone()
|
|
422
|
+
drift_count = int(row["c"]) if row else 0
|
|
423
|
+
except aiosqlite.OperationalError:
|
|
424
|
+
drift_count = 0
|
|
425
|
+
|
|
426
|
+
# Memory ops: count pending operations (same rationale as drift).
|
|
427
|
+
mem_count = 0
|
|
428
|
+
try:
|
|
429
|
+
async with db.execute(
|
|
430
|
+
"SELECT COUNT(*) AS c FROM brain_memory_operations "
|
|
431
|
+
"WHERE cycle_key = ? AND approval_state = 'pending'",
|
|
432
|
+
(resolved_cycle,),
|
|
433
|
+
) as cur:
|
|
434
|
+
row = await cur.fetchone()
|
|
435
|
+
mem_count = int(row["c"]) if row else 0
|
|
436
|
+
except aiosqlite.OperationalError:
|
|
437
|
+
mem_count = 0
|
|
438
|
+
|
|
439
|
+
# Findings: count open findings for this cycle (same rationale).
|
|
440
|
+
findings_count = 0
|
|
441
|
+
try:
|
|
442
|
+
async with db.execute(
|
|
443
|
+
"SELECT COUNT(*) AS c FROM brain_findings "
|
|
444
|
+
"WHERE cycle_key = ? AND approval_state = 'open'",
|
|
445
|
+
(resolved_cycle,),
|
|
446
|
+
) as cur:
|
|
447
|
+
row = await cur.fetchone()
|
|
448
|
+
findings_count = int(row["c"]) if row else 0
|
|
449
|
+
except aiosqlite.OperationalError:
|
|
450
|
+
findings_count = 0
|
|
451
|
+
|
|
452
|
+
# Ingest: count pending ingest items (system-wide, no cycle scope)
|
|
453
|
+
ingest_count = 0
|
|
454
|
+
try:
|
|
455
|
+
async with db.execute(
|
|
456
|
+
"SELECT COUNT(*) AS c FROM ingest_pending WHERE status = 'pending'",
|
|
457
|
+
) as cur:
|
|
458
|
+
row = await cur.fetchone()
|
|
459
|
+
ingest_count = int(row["c"]) if row else 0
|
|
460
|
+
except aiosqlite.OperationalError:
|
|
461
|
+
ingest_count = 0
|
|
462
|
+
|
|
463
|
+
return PipelineCounters(
|
|
464
|
+
cycle_key=resolved_cycle,
|
|
465
|
+
run_id=run_id,
|
|
466
|
+
ingest=ingest_count,
|
|
467
|
+
digest=digest_count,
|
|
468
|
+
journal=journal_count,
|
|
469
|
+
drift=drift_count,
|
|
470
|
+
memory_ops=mem_count,
|
|
471
|
+
findings=findings_count,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _all_run_statuses() -> tuple[str, ...]:
|
|
476
|
+
return _VALID_STATUSES
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
__all__ = [
|
|
480
|
+
"DEFAULT_LIMIT",
|
|
481
|
+
"MAX_LIMIT",
|
|
482
|
+
"fetch_single_run",
|
|
483
|
+
"get_pipeline_counters",
|
|
484
|
+
"list_runs",
|
|
485
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Project → program resolver. Reads programs.yaml at repo root.
|
|
2
|
+
# Plan: sub-01 §9. Single resolver for Digest/Journal/Drift/Memory-Ops/Learn.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import Lock
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from core.api.paths import repo_path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_PROGRAMS_YAML_PATH = repo_path(__file__, "programs.yaml")
|
|
17
|
+
_cache_lock = Lock()
|
|
18
|
+
_cached_map: dict[str, str] | None = None
|
|
19
|
+
_cached_mtime: float | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_programs_map() -> dict[str, str]:
|
|
23
|
+
"""Return {project_slug: program_key}. Empty dict on missing/invalid yaml.
|
|
24
|
+
|
|
25
|
+
Cached by file mtime — re-parses if the file changes on disk.
|
|
26
|
+
"""
|
|
27
|
+
global _cached_map, _cached_mtime
|
|
28
|
+
|
|
29
|
+
path = _PROGRAMS_YAML_PATH
|
|
30
|
+
try:
|
|
31
|
+
mtime = path.stat().st_mtime
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
with _cache_lock:
|
|
34
|
+
_cached_map = {}
|
|
35
|
+
_cached_mtime = None
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
with _cache_lock:
|
|
39
|
+
if _cached_map is not None and _cached_mtime == mtime:
|
|
40
|
+
return _cached_map
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
44
|
+
doc: dict[str, Any] = yaml.safe_load(fh) or {}
|
|
45
|
+
except (OSError, yaml.YAMLError):
|
|
46
|
+
logger.exception("brain.scope: failed to parse %s", path)
|
|
47
|
+
with _cache_lock:
|
|
48
|
+
_cached_map = {}
|
|
49
|
+
_cached_mtime = mtime
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
mapping: dict[str, str] = {}
|
|
53
|
+
if isinstance(doc, dict):
|
|
54
|
+
for program_key, payload in doc.items():
|
|
55
|
+
if not isinstance(program_key, str) or not isinstance(payload, dict):
|
|
56
|
+
continue
|
|
57
|
+
for slug in payload.get("projects") or []:
|
|
58
|
+
if isinstance(slug, str):
|
|
59
|
+
mapping[slug] = program_key
|
|
60
|
+
|
|
61
|
+
with _cache_lock:
|
|
62
|
+
_cached_map = mapping
|
|
63
|
+
_cached_mtime = mtime
|
|
64
|
+
return mapping
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_program(project_slug: str | None) -> str | None:
|
|
68
|
+
"""Return program_key for the given slug, or None if standalone."""
|
|
69
|
+
if not project_slug:
|
|
70
|
+
return None
|
|
71
|
+
return _load_programs_map().get(project_slug)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reset_cache() -> None:
|
|
75
|
+
"""Reset the cache. Test-only helper."""
|
|
76
|
+
global _cached_map, _cached_mtime
|
|
77
|
+
with _cache_lock:
|
|
78
|
+
_cached_map = None
|
|
79
|
+
_cached_mtime = None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Brain v1.0.1 — Source collector subpackage (sub-01 §3 Sources v1).
|
|
2
|
+
# Each module owns one substrate (ingest / git+KG / PiR tasks / handoffs /
|
|
3
|
+
# learnings) and exposes a SourceCollector descriptor.
|
|
4
|
+
#
|
|
5
|
+
# register_all_collectors() is the single startup hook called from
|
|
6
|
+
# api/main.py lifespan BEFORE the first _periodic_brain_jobs tick. It is
|
|
7
|
+
# idempotent: prior collectors are cleared and the canonical five
|
|
8
|
+
# re-registered, so lifespan restarts never accrete duplicates.
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from core.api.services.brain.digest_collector import (
|
|
12
|
+
clear_registered_collectors,
|
|
13
|
+
register_collector,
|
|
14
|
+
)
|
|
15
|
+
from core.api.services.brain.sources.git_kg import git_collector, kg_collector
|
|
16
|
+
from core.api.services.brain.sources.handoffs import handoffs_collector
|
|
17
|
+
from core.api.services.brain.sources.ingestor import ingestor_collector
|
|
18
|
+
from core.api.services.brain.sources.learnings import learnings_collector
|
|
19
|
+
from core.api.services.brain.sources.pir_tasks import pir_tasks_collector
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_all_collectors() -> None:
|
|
23
|
+
"""Clear + register the six v1.0.1 source collectors. Idempotent.
|
|
24
|
+
|
|
25
|
+
git_kg split in 2 separate collectors (git + kg) — CHECK constraint
|
|
26
|
+
brain_source_watermarks.source_system accepts only single tokens
|
|
27
|
+
('git', 'kg', etc.), not composite 'git_kg'. Bugfix 2026-05-16.
|
|
28
|
+
"""
|
|
29
|
+
clear_registered_collectors()
|
|
30
|
+
register_collector(ingestor_collector)
|
|
31
|
+
register_collector(git_collector)
|
|
32
|
+
register_collector(kg_collector)
|
|
33
|
+
register_collector(pir_tasks_collector)
|
|
34
|
+
register_collector(handoffs_collector)
|
|
35
|
+
register_collector(learnings_collector)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"git_collector",
|
|
40
|
+
"kg_collector",
|
|
41
|
+
"handoffs_collector",
|
|
42
|
+
"ingestor_collector",
|
|
43
|
+
"learnings_collector",
|
|
44
|
+
"pir_tasks_collector",
|
|
45
|
+
"register_all_collectors",
|
|
46
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Brain v1.0.1 — Shared helpers for source collectors (sub-01 §3 Sources).
|
|
2
|
+
# These helpers exist to keep individual collector files mechanical and short:
|
|
3
|
+
# substrate-specific reads belong in the collector module, while every-source
|
|
4
|
+
# concerns (timezone normalization, scope resolution, evidence hashing) live
|
|
5
|
+
# here.
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from core.api.services.brain.cycle import evidence_hash as _cycle_evidence_hash
|
|
14
|
+
from core.api.services.brain.cycle import make_event_id as _cycle_make_event_id
|
|
15
|
+
from core.api.services.brain.scope import resolve_program
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def evidence_hash(evidence: dict[str, Any]) -> str:
|
|
19
|
+
"""Re-export of cycle.evidence_hash for collector code locality."""
|
|
20
|
+
return _cycle_evidence_hash(evidence)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_event_id(
|
|
24
|
+
*, cycle_key: str, event_type: str, source_ref: str, evidence: dict[str, Any]
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Wrap cycle.make_event_id so collectors don't have to hash twice."""
|
|
27
|
+
return _cycle_make_event_id(
|
|
28
|
+
cycle_key=cycle_key,
|
|
29
|
+
event_type=event_type,
|
|
30
|
+
source_ref=source_ref,
|
|
31
|
+
evidence_hash_hex=_cycle_evidence_hash(evidence),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_iso(value: Any) -> datetime | None:
|
|
36
|
+
"""Best-effort ISO-8601 parse → aware UTC datetime.
|
|
37
|
+
|
|
38
|
+
Handles the three flavours we encounter across substrate tables:
|
|
39
|
+
* tasks.updated_at: `datetime('now')` → naive LOCAL string. Treated as
|
|
40
|
+
UTC here per repo convention (mirror inbox_digest_jobs).
|
|
41
|
+
* learnings.updated_at: optional `datetime('now','utc')`.
|
|
42
|
+
* inbox_items.updated_at: same as tasks.
|
|
43
|
+
"""
|
|
44
|
+
if value is None:
|
|
45
|
+
return None
|
|
46
|
+
if isinstance(value, datetime):
|
|
47
|
+
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
48
|
+
try:
|
|
49
|
+
raw = str(value).replace("Z", "+00:00")
|
|
50
|
+
dt = datetime.fromisoformat(raw)
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
return None
|
|
53
|
+
if dt.tzinfo is None:
|
|
54
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
55
|
+
return dt.astimezone(timezone.utc)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def stable_short_hash(value: str, *, length: int = 12) -> str:
|
|
59
|
+
"""BLAKE2b-prefixed deterministic short hash (filename anonymizer)."""
|
|
60
|
+
return hashlib.blake2b(value.encode("utf-8"), digest_size=length).hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def canonical_payload(payload: dict[str, Any]) -> str:
|
|
64
|
+
"""Canonical JSON used for evidence hashing — mirrors cycle.canonical_evidence."""
|
|
65
|
+
return json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_source_project(slug: str | None) -> tuple[str | None, str | None]:
|
|
69
|
+
"""Return (source_project_slug, program_key) — both possibly None.
|
|
70
|
+
|
|
71
|
+
Centralised so each collector calls the same resolver — Drift/Memory-Ops
|
|
72
|
+
can later assume program_key is consistent with source_project.
|
|
73
|
+
"""
|
|
74
|
+
if not slug:
|
|
75
|
+
return None, None
|
|
76
|
+
return slug, resolve_program(slug)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"canonical_payload",
|
|
81
|
+
"evidence_hash",
|
|
82
|
+
"make_event_id",
|
|
83
|
+
"normalize_iso",
|
|
84
|
+
"resolve_source_project",
|
|
85
|
+
"stable_short_hash",
|
|
86
|
+
]
|