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,364 @@
|
|
|
1
|
+
"""Codex JSONL metrics provider.
|
|
2
|
+
|
|
3
|
+
Codex CLI stores session transcripts under ~/.codex/sessions/YYYY/MM/DD/*.jsonl.
|
|
4
|
+
The JSONL includes a session_meta event with the stable session id and periodic
|
|
5
|
+
token_count events with current context and cumulative token usage.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
from core.api.services.claude_metrics import SessionMetrics
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions"
|
|
23
|
+
_WORKSPACE_ROOT = os.environ.get("MARVIS_WORKSPACE_ROOT", str(Path.home() / "workspace"))
|
|
24
|
+
_PROC_ROOT = Path("/proc")
|
|
25
|
+
_CODEX_DETECT_GRACE_SECONDS = 5.0
|
|
26
|
+
_CODEX_DETECT_WINDOW_SECONDS = 60.0
|
|
27
|
+
_SESSION_ID_RE = re.compile(
|
|
28
|
+
r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class CodexSessionMeta:
|
|
35
|
+
session_id: str
|
|
36
|
+
timestamp: datetime
|
|
37
|
+
cwd: str | None
|
|
38
|
+
path: Path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_timestamp(value: str | None) -> datetime | None:
|
|
42
|
+
if not value:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
46
|
+
except ValueError:
|
|
47
|
+
return None
|
|
48
|
+
if parsed.tzinfo is None:
|
|
49
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
50
|
+
return parsed.astimezone(timezone.utc)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _iter_session_files() -> Iterable[Path]:
|
|
54
|
+
if not CODEX_SESSIONS_DIR.is_dir():
|
|
55
|
+
return ()
|
|
56
|
+
return CODEX_SESSIONS_DIR.glob("**/*.jsonl")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _session_id_from_path(path: Path) -> str | None:
|
|
60
|
+
match = _SESSION_ID_RE.search(path.stem)
|
|
61
|
+
return match.group(1).lower() if match else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_codex_session_path(path: Path) -> bool:
|
|
65
|
+
if path.suffix != ".jsonl":
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
path.resolve(strict=False).relative_to(
|
|
69
|
+
CODEX_SESSIONS_DIR.resolve(strict=False)
|
|
70
|
+
)
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_meta(path: Path) -> CodexSessionMeta | None:
|
|
77
|
+
try:
|
|
78
|
+
with path.open("r", encoding="utf-8", errors="replace") as f:
|
|
79
|
+
for line in f:
|
|
80
|
+
try:
|
|
81
|
+
obj = json.loads(line)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
continue
|
|
84
|
+
if obj.get("type") != "session_meta":
|
|
85
|
+
continue
|
|
86
|
+
payload = obj.get("payload") or {}
|
|
87
|
+
session_id = payload.get("id")
|
|
88
|
+
timestamp = _parse_timestamp(payload.get("timestamp"))
|
|
89
|
+
if not session_id or timestamp is None:
|
|
90
|
+
return None
|
|
91
|
+
return CodexSessionMeta(
|
|
92
|
+
session_id=session_id,
|
|
93
|
+
timestamp=timestamp,
|
|
94
|
+
cwd=payload.get("cwd"),
|
|
95
|
+
path=path,
|
|
96
|
+
)
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def find_session_path(session_id: str) -> Path | None:
|
|
103
|
+
"""Find the JSONL file for a Codex session id."""
|
|
104
|
+
if not session_id or not CODEX_SESSIONS_DIR.is_dir():
|
|
105
|
+
return None
|
|
106
|
+
for path in _iter_session_files():
|
|
107
|
+
if session_id in path.stem:
|
|
108
|
+
return path
|
|
109
|
+
for path in _iter_session_files():
|
|
110
|
+
meta = _read_meta(path)
|
|
111
|
+
if meta and meta.session_id == session_id:
|
|
112
|
+
return path
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _iter_process_tree_pids(root_pid: int) -> Iterable[int]:
|
|
117
|
+
"""Yield root_pid and descendants using Linux procfs child lists."""
|
|
118
|
+
seen: set[int] = set()
|
|
119
|
+
stack = [root_pid]
|
|
120
|
+
while stack:
|
|
121
|
+
pid = stack.pop()
|
|
122
|
+
if pid in seen or pid <= 0:
|
|
123
|
+
continue
|
|
124
|
+
seen.add(pid)
|
|
125
|
+
yield pid
|
|
126
|
+
|
|
127
|
+
children_path = _PROC_ROOT / str(pid) / "task" / str(pid) / "children"
|
|
128
|
+
try:
|
|
129
|
+
children = children_path.read_text(encoding="utf-8").split()
|
|
130
|
+
except OSError:
|
|
131
|
+
continue
|
|
132
|
+
for child in children:
|
|
133
|
+
try:
|
|
134
|
+
stack.append(int(child))
|
|
135
|
+
except ValueError:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _iter_open_session_files(pid: int) -> Iterable[Path]:
|
|
140
|
+
fd_dir = _PROC_ROOT / str(pid) / "fd"
|
|
141
|
+
try:
|
|
142
|
+
fds = list(fd_dir.iterdir())
|
|
143
|
+
except OSError:
|
|
144
|
+
return ()
|
|
145
|
+
|
|
146
|
+
paths: list[Path] = []
|
|
147
|
+
for fd_path in fds:
|
|
148
|
+
try:
|
|
149
|
+
target = fd_path.readlink()
|
|
150
|
+
except OSError:
|
|
151
|
+
continue
|
|
152
|
+
target_text = str(target)
|
|
153
|
+
if target_text.endswith(" (deleted)"):
|
|
154
|
+
target_text = target_text.removesuffix(" (deleted)")
|
|
155
|
+
target_path = Path(target_text)
|
|
156
|
+
if not target_path.is_absolute() or not _is_codex_session_path(target_path):
|
|
157
|
+
continue
|
|
158
|
+
paths.append(target_path)
|
|
159
|
+
return paths
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def detect_codex_for_process(
|
|
163
|
+
pid: int | None,
|
|
164
|
+
already_linked: Iterable[str] = (),
|
|
165
|
+
) -> str | None:
|
|
166
|
+
"""Detect the Codex session id from a live process tree's open JSONL fd.
|
|
167
|
+
|
|
168
|
+
Resumed Codex sessions keep writing to their original JSONL, whose
|
|
169
|
+
session_meta timestamp can be days older than the tmux pane. Inspecting the
|
|
170
|
+
live process fds is the stable correlation path for those sessions.
|
|
171
|
+
"""
|
|
172
|
+
if pid is None or pid <= 0:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
linked = set(already_linked)
|
|
176
|
+
seen_paths: set[Path] = set()
|
|
177
|
+
candidates: list[tuple[float, str]] = []
|
|
178
|
+
for process_pid in _iter_process_tree_pids(pid):
|
|
179
|
+
for path in _iter_open_session_files(process_pid):
|
|
180
|
+
try:
|
|
181
|
+
canonical = path.resolve(strict=False)
|
|
182
|
+
except OSError:
|
|
183
|
+
canonical = path
|
|
184
|
+
if canonical in seen_paths:
|
|
185
|
+
continue
|
|
186
|
+
seen_paths.add(canonical)
|
|
187
|
+
|
|
188
|
+
meta = _read_meta(path)
|
|
189
|
+
session_id = meta.session_id if meta else _session_id_from_path(path)
|
|
190
|
+
if not session_id or session_id in linked:
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
mtime = path.stat().st_mtime
|
|
194
|
+
except OSError:
|
|
195
|
+
mtime = 0.0
|
|
196
|
+
candidates.append((mtime, session_id))
|
|
197
|
+
|
|
198
|
+
candidates.sort(reverse=True)
|
|
199
|
+
return candidates[0][1] if candidates else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def detect_codex_for_session(
|
|
203
|
+
pane_start_epoch: float | None,
|
|
204
|
+
cwd: str | None = None,
|
|
205
|
+
already_linked: Iterable[str] = (),
|
|
206
|
+
) -> str | None:
|
|
207
|
+
"""Detect the Codex session id that started shortly after a tmux pane.
|
|
208
|
+
|
|
209
|
+
Codex JSONL files do not currently record the tmux session name. The safest
|
|
210
|
+
stable correlation available is the launch timestamp: Console-created Codex
|
|
211
|
+
sessions write session_meta within a few seconds of tmux creation. The match
|
|
212
|
+
window is intentionally bounded to avoid stealing a later unrelated Codex
|
|
213
|
+
session from an idle pane that never received a prompt.
|
|
214
|
+
"""
|
|
215
|
+
if pane_start_epoch is None:
|
|
216
|
+
return None
|
|
217
|
+
linked = set(already_linked)
|
|
218
|
+
lower = pane_start_epoch - _CODEX_DETECT_GRACE_SECONDS
|
|
219
|
+
upper = pane_start_epoch + _CODEX_DETECT_WINDOW_SECONDS
|
|
220
|
+
candidates: list[CodexSessionMeta] = []
|
|
221
|
+
for path in _iter_session_files():
|
|
222
|
+
meta = _read_meta(path)
|
|
223
|
+
if not meta or meta.session_id in linked:
|
|
224
|
+
continue
|
|
225
|
+
started = meta.timestamp.timestamp()
|
|
226
|
+
if lower <= started <= upper:
|
|
227
|
+
# Codex currently launches from the shared workspace. If a future
|
|
228
|
+
# version records a provider-specific cwd, prefer exact matches but
|
|
229
|
+
# do not reject workspace-root sessions.
|
|
230
|
+
if (
|
|
231
|
+
cwd
|
|
232
|
+
and meta.cwd
|
|
233
|
+
and meta.cwd != cwd
|
|
234
|
+
and meta.cwd != _WORKSPACE_ROOT
|
|
235
|
+
):
|
|
236
|
+
continue
|
|
237
|
+
candidates.append(meta)
|
|
238
|
+
candidates.sort(key=lambda item: item.timestamp)
|
|
239
|
+
return candidates[0].session_id if candidates else None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def parse_session_file(path: Path) -> SessionMetrics | None:
|
|
243
|
+
if not path.exists():
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
session_id: str | None = None
|
|
247
|
+
model: str | None = None
|
|
248
|
+
first_ts: str | None = None
|
|
249
|
+
last_ts: str | None = None
|
|
250
|
+
message_count = 0
|
|
251
|
+
last_token_info: dict | None = None
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with path.open("r", encoding="utf-8", errors="replace") as f:
|
|
255
|
+
for line in f:
|
|
256
|
+
try:
|
|
257
|
+
obj = json.loads(line)
|
|
258
|
+
except json.JSONDecodeError:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
obj_ts = obj.get("timestamp")
|
|
262
|
+
if obj_ts and first_ts is None:
|
|
263
|
+
first_ts = obj_ts
|
|
264
|
+
if obj_ts:
|
|
265
|
+
last_ts = obj_ts
|
|
266
|
+
|
|
267
|
+
obj_type = obj.get("type")
|
|
268
|
+
payload = obj.get("payload") or {}
|
|
269
|
+
if obj_type == "session_meta":
|
|
270
|
+
session_id = payload.get("id") or session_id
|
|
271
|
+
first_ts = payload.get("timestamp") or first_ts
|
|
272
|
+
elif obj_type == "turn_context":
|
|
273
|
+
model = payload.get("model") or model
|
|
274
|
+
elif obj_type == "event_msg":
|
|
275
|
+
event_type = payload.get("type")
|
|
276
|
+
if event_type == "agent_message":
|
|
277
|
+
message_count += 1
|
|
278
|
+
elif event_type == "token_count":
|
|
279
|
+
info = payload.get("info")
|
|
280
|
+
if isinstance(info, dict):
|
|
281
|
+
last_token_info = info
|
|
282
|
+
except OSError:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
if not session_id:
|
|
286
|
+
meta = _read_meta(path)
|
|
287
|
+
session_id = (
|
|
288
|
+
meta.session_id if meta else (_session_id_from_path(path) or path.stem)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if not last_token_info:
|
|
292
|
+
return SessionMetrics(
|
|
293
|
+
conversation_id=session_id,
|
|
294
|
+
model=model,
|
|
295
|
+
message_count=message_count,
|
|
296
|
+
first_timestamp=first_ts,
|
|
297
|
+
last_timestamp=last_ts,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
total_usage = last_token_info.get("total_token_usage") or {}
|
|
301
|
+
last_usage = last_token_info.get("last_token_usage") or {}
|
|
302
|
+
input_tokens = int(total_usage.get("input_tokens") or 0)
|
|
303
|
+
output_tokens = int(total_usage.get("output_tokens") or 0)
|
|
304
|
+
cache_read_tokens = int(total_usage.get("cached_input_tokens") or 0)
|
|
305
|
+
reasoning_tokens = int(total_usage.get("reasoning_output_tokens") or 0)
|
|
306
|
+
|
|
307
|
+
window = int(last_token_info.get("model_context_window") or 0)
|
|
308
|
+
last_total = int(last_usage.get("total_tokens") or 0)
|
|
309
|
+
if window > 0 and last_total > 0:
|
|
310
|
+
context_pct = round(min((last_total / window) * 100, 100.0), 1)
|
|
311
|
+
else:
|
|
312
|
+
context_pct = 0.0
|
|
313
|
+
|
|
314
|
+
duration_minutes = 0.0
|
|
315
|
+
first_dt = _parse_timestamp(first_ts)
|
|
316
|
+
last_dt = _parse_timestamp(last_ts)
|
|
317
|
+
if first_dt and last_dt and last_dt >= first_dt:
|
|
318
|
+
duration_minutes = round((last_dt - first_dt).total_seconds() / 60, 1)
|
|
319
|
+
|
|
320
|
+
return SessionMetrics(
|
|
321
|
+
conversation_id=session_id,
|
|
322
|
+
model=model,
|
|
323
|
+
context_pct=context_pct,
|
|
324
|
+
cost_usd=0.0,
|
|
325
|
+
message_count=message_count,
|
|
326
|
+
input_tokens=input_tokens,
|
|
327
|
+
output_tokens=output_tokens,
|
|
328
|
+
cache_read_tokens=cache_read_tokens,
|
|
329
|
+
cache_write_tokens=0,
|
|
330
|
+
first_timestamp=first_ts,
|
|
331
|
+
last_timestamp=last_ts,
|
|
332
|
+
duration_minutes=duration_minutes,
|
|
333
|
+
context_pct_real=context_pct,
|
|
334
|
+
context_pct_scaled=None,
|
|
335
|
+
cost_conversation_usd=0.0,
|
|
336
|
+
cost_session_usd=None,
|
|
337
|
+
reasoning_tokens=reasoning_tokens,
|
|
338
|
+
working_seconds_msg=None,
|
|
339
|
+
pricing_version=None,
|
|
340
|
+
cost_conversation_equivalent_usd=None,
|
|
341
|
+
cost_session_equivalent_usd=None,
|
|
342
|
+
cost_equivalent_pricing_version=None,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class CodexMetricsProvider:
|
|
347
|
+
name = "codex"
|
|
348
|
+
|
|
349
|
+
def parse_session(
|
|
350
|
+
self,
|
|
351
|
+
session_id: str,
|
|
352
|
+
cwd: str | None = None,
|
|
353
|
+
) -> SessionMetrics | None:
|
|
354
|
+
_ = cwd
|
|
355
|
+
path = find_session_path(session_id)
|
|
356
|
+
return parse_session_file(path) if path else None
|
|
357
|
+
|
|
358
|
+
def get_last_context_pct(
|
|
359
|
+
self,
|
|
360
|
+
session_id: str,
|
|
361
|
+
cwd: str | None = None,
|
|
362
|
+
) -> float | None:
|
|
363
|
+
metrics = self.parse_session(session_id, cwd)
|
|
364
|
+
return metrics.context_pct_real if metrics else None
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# api/services/conversation_reader.py
|
|
2
|
+
# v2.1.0 - 2026-03-04 - Extract AskUserQuestion tool_use blocks for DevX classifier
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
CLAUDE_PROJECTS_PATH = Path.home() / ".claude" / "projects"
|
|
10
|
+
_UUID_RE = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_conversation_id(conversation_id: str) -> bool:
|
|
14
|
+
"""Validates that conversation_id is a safe UUID-like string (no path traversal)."""
|
|
15
|
+
return bool(_UUID_RE.match(conversation_id))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _find_jsonl_path(conversation_id: str) -> Path | None:
|
|
19
|
+
"""Finds JSONL path safely using direct path construction (no rglob)."""
|
|
20
|
+
if not CLAUDE_PROJECTS_PATH.exists():
|
|
21
|
+
return None
|
|
22
|
+
for project_dir in CLAUDE_PROJECTS_PATH.iterdir():
|
|
23
|
+
if not project_dir.is_dir():
|
|
24
|
+
continue
|
|
25
|
+
candidate = project_dir / f"{conversation_id}.jsonl"
|
|
26
|
+
if candidate.exists() and CLAUDE_PROJECTS_PATH in candidate.resolve().parents:
|
|
27
|
+
return candidate
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_conversation_sync(
|
|
32
|
+
jsonl_path: Path,
|
|
33
|
+
limit: int = 20,
|
|
34
|
+
role: str | None = None,
|
|
35
|
+
since: str | None = None,
|
|
36
|
+
) -> list[dict]:
|
|
37
|
+
"""Sync: reads JSONL tail, filters, returns last N messages."""
|
|
38
|
+
messages = []
|
|
39
|
+
file_size = jsonl_path.stat().st_size
|
|
40
|
+
tail_bytes = min(file_size, 524288) # 512KB tail
|
|
41
|
+
|
|
42
|
+
with jsonl_path.open('rb') as f:
|
|
43
|
+
if tail_bytes < file_size:
|
|
44
|
+
f.seek(file_size - tail_bytes)
|
|
45
|
+
f.readline() # skip partial first line
|
|
46
|
+
raw_lines = f.readlines()
|
|
47
|
+
|
|
48
|
+
for raw_line in raw_lines:
|
|
49
|
+
try:
|
|
50
|
+
line = raw_line.decode('utf-8', errors='replace')
|
|
51
|
+
obj = json.loads(line)
|
|
52
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
53
|
+
continue
|
|
54
|
+
if obj.get("type") not in ("user", "assistant"):
|
|
55
|
+
continue
|
|
56
|
+
if role and obj.get("type") != role:
|
|
57
|
+
continue
|
|
58
|
+
if since and obj.get("timestamp", "") < since:
|
|
59
|
+
continue
|
|
60
|
+
msg = obj.get("message", {})
|
|
61
|
+
content = msg.get("content", "")
|
|
62
|
+
text = ""
|
|
63
|
+
if isinstance(content, list):
|
|
64
|
+
for c in content:
|
|
65
|
+
if isinstance(c, dict) and c.get("type") == "text":
|
|
66
|
+
text += c.get("text", "")
|
|
67
|
+
elif isinstance(c, dict) and c.get("type") == "tool_use" and c.get("name") == "AskUserQuestion":
|
|
68
|
+
for q in c.get("input", {}).get("questions", []):
|
|
69
|
+
question = q.get("question", "")
|
|
70
|
+
opts = q.get("options", [])
|
|
71
|
+
opt_lines = [f"{i}. {o.get('label', '')}" for i, o in enumerate(opts, 1)]
|
|
72
|
+
text += f"[AskUserQuestion] {question}\n" + "\n".join(opt_lines) + "\n"
|
|
73
|
+
else:
|
|
74
|
+
text = str(content)
|
|
75
|
+
messages.append({
|
|
76
|
+
"role": obj.get("type"),
|
|
77
|
+
"text": text[:2000],
|
|
78
|
+
"timestamp": obj.get("timestamp"),
|
|
79
|
+
})
|
|
80
|
+
return messages[-limit:]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def read_conversation(
|
|
84
|
+
conversation_id: str,
|
|
85
|
+
limit: int = 20,
|
|
86
|
+
role: str | None = None,
|
|
87
|
+
since: str | None = None,
|
|
88
|
+
) -> list[dict] | None:
|
|
89
|
+
"""Async wrapper: finds JSONL file safely, reads conversation messages."""
|
|
90
|
+
if not _validate_conversation_id(conversation_id):
|
|
91
|
+
raise ValueError(f"Invalid conversation_id format: {conversation_id!r}")
|
|
92
|
+
jsonl_path = _find_jsonl_path(conversation_id)
|
|
93
|
+
if jsonl_path is None:
|
|
94
|
+
return None
|
|
95
|
+
loop = asyncio.get_running_loop()
|
|
96
|
+
try:
|
|
97
|
+
return await asyncio.wait_for(
|
|
98
|
+
loop.run_in_executor(None, _read_conversation_sync, jsonl_path, limit, role, since),
|
|
99
|
+
timeout=30.0,
|
|
100
|
+
)
|
|
101
|
+
except TimeoutError:
|
|
102
|
+
return []
|