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.
Files changed (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,156 @@
1
+ # v2.0.0 - 2026-05-27 - S1 F1.11: thin adapter over use_cases.handoffs
2
+ # v1.3.1 - 2026-04-17 - fix node_id double prefix in deep handoff bundle
3
+ """HTTP adapter for the handoffs domain (S1 collapse-runtime).
4
+
5
+ This router is a thin transport adapter. All search/read/validation/visibility
6
+ logic lives in :mod:`core.api.use_cases.handoffs` (pure, fastapi-free). Each
7
+ handler:
8
+
9
+ 1. resolves identity into a :class:`CallerContext` (handoffs have no human-only
10
+ gate, so ``is_human_session=False`` — no ``Request`` needed);
11
+ 2. resolves visibility at the boundary (DECISION 1) and passes it in;
12
+ 3. calls the use_case inside ``try/except ServiceError`` -> ``to_http``;
13
+ 4. owns the ``deep`` KG enrichment: rate-limit + audit log + ``kg_context``
14
+ (DECISION 2).
15
+
16
+ ``get_current_user_or_agent`` stays as the ``Depends`` on ``search_handoffs`` so
17
+ the agent-facing auth regression test (``test_agent_facing_auth_dependencies``)
18
+ keeps passing. The pure helpers + DTOs are re-exported from the use_case so any
19
+ existing importer keeps working.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+
25
+ import aiosqlite
26
+ from fastapi import APIRouter, Depends, Query
27
+
28
+ from core.api.db import get_db, get_vec_db
29
+ from core.api.models import HandoffSearchResult, UserInfo
30
+ from core.api.rbac import require_role
31
+ from core.api.routers._adapter import to_http
32
+ from core.api.security import get_current_user_or_agent
33
+ from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
34
+ from core.api.services.kg.lens import build_kg_context_for_handoff
35
+ from core.api.use_cases import handoffs as uc
36
+ from core.api.use_cases._context import CallerContext
37
+ from core.api.use_cases._errors import ServiceError
38
+ from core.api.visibility import get_visible_projects
39
+
40
+ # Re-export the pure helpers + constants from the use_case so existing importers
41
+ # (and any future callers) keep importing them from this router path unchanged.
42
+ from core.api.use_cases.handoffs import ( # noqa: F401 (re-export surface)
43
+ _HANDOFF_FILENAME_RE,
44
+ _SNIPPET_RADIUS,
45
+ _count_matches,
46
+ _extract_snippet,
47
+ _iter_all_slugs,
48
+ _parse_frontmatter,
49
+ _safe_read,
50
+ _search_semantic,
51
+ )
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ router = APIRouter(prefix="/api/v1/handoffs", tags=["handoffs"])
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Endpoints (thin adapters)
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ @router.get("/search", response_model=list[HandoffSearchResult])
64
+ async def search_handoffs(
65
+ q: str | None = Query(None, description="Full-text query (searches filename, tags, body)"),
66
+ project: str | None = Query(None, description="Filter by project slug"),
67
+ tags: str | None = Query(None, description="Comma-separated tags (OR logic)"),
68
+ date_start: str | None = Query(None, description="Start date filter YYYY-MM-DD (inclusive)"),
69
+ date_end: str | None = Query(None, description="End date filter YYYY-MM-DD (inclusive)"),
70
+ limit: int = Query(20, ge=1, le=200, description="Max results"),
71
+ user: UserInfo = Depends(get_current_user_or_agent),
72
+ db: aiosqlite.Connection = Depends(get_db),
73
+ vec_db: aiosqlite.Connection = Depends(get_vec_db),
74
+ ) -> list[HandoffSearchResult]:
75
+ """Full-text search across handoff files.
76
+
77
+ Parses frontmatter YAML from every handoff-*.md file found in project
78
+ memory/ directories and filters by query, project, tags, and date range.
79
+ Results are ranked by match count (descending), then date (descending).
80
+ """
81
+ # DECISION 1 (the visibility template): resolve visibility at the boundary
82
+ # (needs UserInfo.teams, not carried by CallerContext) and pass it in; the
83
+ # use_case enforces it by silent filtering, exactly as the original router did.
84
+ visible_projects = await get_visible_projects(db, user)
85
+
86
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
87
+ try:
88
+ return await uc.search_handoffs(
89
+ ctx,
90
+ db,
91
+ vec_db,
92
+ q=q,
93
+ project=project,
94
+ tags=tags,
95
+ date_start=date_start,
96
+ date_end=date_end,
97
+ limit=limit,
98
+ visible_projects=visible_projects,
99
+ )
100
+ except ServiceError as e:
101
+ raise to_http(e)
102
+
103
+
104
+ @router.get("/{project_slug}/{filename}")
105
+ async def get_handoff(
106
+ project_slug: str,
107
+ filename: str,
108
+ deep: bool = Query(False, description="If true, append kg_context from KG Inline Lens"),
109
+ user: UserInfo = Depends(get_current_user_or_agent),
110
+ db: aiosqlite.Connection = Depends(get_db),
111
+ ) -> dict:
112
+ """Get a single handoff file by project slug and filename.
113
+
114
+ Returns frontmatter (parsed YAML) + body text. With ?deep=true also
115
+ returns kg_context built via build_kg_context_for_handoff().
116
+ """
117
+ # DECISION 1: resolve visibility at the boundary; the use_case raises
118
+ # NotFoundError (404) when the project is not visible (mirrors the original
119
+ # check_project_access — 404 not 403, does not reveal existence).
120
+ visible_projects = await get_visible_projects(db, user)
121
+
122
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
123
+ try:
124
+ result = await uc.get_handoff(
125
+ ctx,
126
+ db,
127
+ project_slug=project_slug,
128
+ filename=filename,
129
+ visible_projects=visible_projects,
130
+ )
131
+ except ServiceError as e:
132
+ raise to_http(e)
133
+
134
+ # DECISION 2: deep KG enrichment is a per-surface adapter concern.
135
+ if deep:
136
+ check_deep_rate_limit(user.username)
137
+ log_kg_deep_access(user.username, "get_handoff", filename)
138
+ # Builder normalizes raw filename (strips .md + handoff- prefix, then
139
+ # prepends handoff:artifact:). Do not pre-prefix here.
140
+ result["kg_context"] = await build_kg_context_for_handoff(db, filename, deep=True)
141
+
142
+ return result
143
+
144
+
145
+ @router.post("/reindex")
146
+ async def reindex_handoffs(
147
+ project: str = Query(..., description="Project slug to reindex"),
148
+ user: UserInfo = Depends(require_role("admin", "super_admin")), # Bearer-accessible for deploy scripts
149
+ vec_db: aiosqlite.Connection = Depends(get_vec_db),
150
+ ) -> dict:
151
+ """On-demand reindex of handoff embeddings for a project. Admin only."""
152
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
153
+ try:
154
+ return await uc.reindex_handoffs(ctx, vec_db, project=project)
155
+ except ServiceError as e:
156
+ raise to_http(e)
@@ -0,0 +1,496 @@
1
+ # v1.1.0 - 2026-04-14 - Single-writer: reclassify_inbox_items uses get_write_db (batch 5/6)
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import time
6
+ from collections import defaultdict
7
+ from typing import Literal
8
+
9
+ import aiosqlite
10
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
11
+ from pydantic import BaseModel, Field
12
+
13
+ from core.api.db import get_db, get_write_db, write_db
14
+ from core.api.models import (
15
+ InboxGmailSyncCandidate,
16
+ InboxGmailSyncCompleteRequest,
17
+ InboxIngestBatchRequest,
18
+ InboxIngestBatchResponse,
19
+ InboxIngestRequest,
20
+ InboxIngestResponse,
21
+ InboxItemDetail,
22
+ InboxItemSummary,
23
+ InboxStatsResponse,
24
+ InboxStatusUpdateRequest,
25
+ InboxTaxonomyUpdateRequest,
26
+ InboxTaxonomyUpdateResponse,
27
+ InboxTriageDecisionRequest,
28
+ InboxTriageDecisionResponse,
29
+ UserInfo,
30
+ )
31
+ from core.api.rbac import require_role
32
+ from core.api.security import get_current_user_or_agent, require_agent_token_scope
33
+ from core.api.config import settings
34
+ from core.api.services.inbox import ingest_item, ingest_items_batch
35
+ from core.api.services.inbox_llm_classifier import schedule_classification
36
+ from core.api.services.inbox_sources import (
37
+ create_source,
38
+ delete_source,
39
+ get_source,
40
+ get_source_metrics,
41
+ list_sources,
42
+ update_source,
43
+ validate_public_url,
44
+ )
45
+ from core.api.services.inbox_digest import get_digest_stats, list_digest_items
46
+ from core.api.services.inbox_digest_jobs import recompute_digest_now
47
+ from core.api.services.inbox_gmail_sync import (
48
+ list_gmail_sync_candidates,
49
+ mark_gmail_sync_complete,
50
+ )
51
+ from core.api.services.inbox_tldr import get_or_generate_deep_research, get_or_generate_tldr
52
+ from core.api.services.inbox_triage import (
53
+ apply_triage_decision,
54
+ get_inbox_item_detail,
55
+ get_inbox_stats,
56
+ get_source_scores,
57
+ get_unread_count,
58
+ list_inbox_items,
59
+ update_inbox_status,
60
+ update_inbox_taxonomy,
61
+ )
62
+
63
+ logger = logging.getLogger(__name__)
64
+
65
+ router = APIRouter(prefix="/api/v1/inbox", tags=["inbox"])
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Rate limit on POST /inbox/ingest (PR B)
69
+ # ---------------------------------------------------------------------------
70
+ # In-memory sliding window, per-process. 1000 req / 60s per identity — tuned
71
+ # for n8n RSS batch bursts (hundreds of articles per poll cycle) while still
72
+ # providing an emergency cap against runaway cost attacks. At higher sustained
73
+ # rates the LLM cost guard (app_settings daily cap) becomes the binding limit.
74
+ # Good enough for a single-worker FastAPI deployment; swap to Redis if we go
75
+ # multi-worker.
76
+ _INGEST_RATE_LIMITS: dict[str, list[float]] = defaultdict(list)
77
+ _INGEST_RATE_LIMITS_MAX_KEYS = 1000
78
+
79
+
80
+ def _check_ingest_rate_limit(
81
+ identity: str,
82
+ *,
83
+ limit: int = 1000,
84
+ window: float = 60.0,
85
+ ) -> None:
86
+ now = time.time()
87
+ bucket = _INGEST_RATE_LIMITS[identity]
88
+ # Drop expired timestamps in place
89
+ fresh = [t for t in bucket if now - t < window]
90
+ _INGEST_RATE_LIMITS[identity] = fresh
91
+ if len(fresh) >= limit:
92
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
93
+ fresh.append(now)
94
+ # Evict stale keys opportunistically
95
+ if len(_INGEST_RATE_LIMITS) > _INGEST_RATE_LIMITS_MAX_KEYS:
96
+ stale = [
97
+ k for k, v in _INGEST_RATE_LIMITS.items() if not v or (now - v[-1]) > window
98
+ ]
99
+ for k in stale:
100
+ del _INGEST_RATE_LIMITS[k]
101
+
102
+
103
+ class SourceCreateRequest(BaseModel):
104
+ name: str = Field(..., min_length=1, max_length=200)
105
+ source_key: str = Field(..., min_length=1, max_length=200)
106
+ feed_url: str | None = Field(None, max_length=500)
107
+ source_type: str = Field("rss", pattern=r"^(rss|email|manual|api|legacy)$")
108
+
109
+
110
+ class SourceUpdateRequest(BaseModel):
111
+ name: str | None = Field(None, min_length=1, max_length=200)
112
+ feed_url: str | None = Field(None, max_length=500)
113
+ active: bool | None = None
114
+
115
+
116
+ @router.post(
117
+ "/ingest", response_model=InboxIngestResponse, status_code=status.HTTP_201_CREATED
118
+ )
119
+ async def ingest_inbox_item(
120
+ body: InboxIngestRequest,
121
+ user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
122
+ ) -> InboxIngestResponse:
123
+ _check_ingest_rate_limit(f"ingest:{user.username}", limit=1000, window=60)
124
+ async with write_db(label="inbox.ingest.single") as db:
125
+ return await ingest_item(body, user=user, db=db)
126
+
127
+
128
+ @router.post(
129
+ "/ingest/batch",
130
+ response_model=InboxIngestBatchResponse,
131
+ status_code=status.HTTP_200_OK,
132
+ )
133
+ async def ingest_inbox_items_batch(
134
+ body: InboxIngestBatchRequest,
135
+ user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
136
+ ) -> InboxIngestBatchResponse:
137
+ # Rate limit accounts each item individually so a 500-item batch still
138
+ # respects the 1000/60s identity cap. A single large batch is one lock
139
+ # acquisition + one fsync, so this is intentionally generous.
140
+ for _ in range(len(body.items)):
141
+ _check_ingest_rate_limit(f"ingest:{user.username}", limit=1000, window=60)
142
+ async with write_db(label="inbox.ingest.batch") as db:
143
+ return await ingest_items_batch(body, user=user, db=db)
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Sources CRUD (PR B)
148
+ # ---------------------------------------------------------------------------
149
+
150
+
151
+ @router.get("/sources")
152
+ async def list_inbox_sources(
153
+ active_only: bool = Query(False),
154
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
155
+ db: aiosqlite.Connection = Depends(get_db),
156
+ ) -> list[dict]:
157
+ return await list_sources(
158
+ db, user.workspace_id or "ws_default", active_only=active_only
159
+ )
160
+
161
+
162
+ @router.get("/sources/{source_id}")
163
+ async def get_inbox_source(
164
+ source_id: str,
165
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
166
+ db: aiosqlite.Connection = Depends(get_db),
167
+ ) -> dict:
168
+ result = await get_source(db, user.workspace_id or "ws_default", source_id)
169
+ if result is None:
170
+ raise HTTPException(status_code=404, detail="Source not found")
171
+ return result
172
+
173
+
174
+ @router.get("/sources/{source_id}/metrics")
175
+ async def get_inbox_source_metrics_endpoint(
176
+ source_id: str,
177
+ range: Literal["24h", "7d", "30d", "total"] = Query("total"),
178
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
179
+ db: aiosqlite.Connection = Depends(get_db),
180
+ ) -> dict:
181
+ return await get_source_metrics(
182
+ db, user.workspace_id or "ws_default", source_id, range=range
183
+ )
184
+
185
+
186
+ @router.post("/sources")
187
+ async def create_inbox_source(
188
+ body: SourceCreateRequest,
189
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
190
+ db: aiosqlite.Connection = Depends(get_write_db),
191
+ ) -> dict:
192
+ if body.feed_url:
193
+ validate_public_url(body.feed_url)
194
+ return await create_source(
195
+ db,
196
+ user.workspace_id or "ws_default",
197
+ name=body.name,
198
+ source_key=body.source_key,
199
+ feed_url=body.feed_url,
200
+ source_type=body.source_type,
201
+ )
202
+
203
+
204
+ @router.patch("/sources/{source_id}")
205
+ async def update_inbox_source(
206
+ source_id: str,
207
+ body: SourceUpdateRequest,
208
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
209
+ db: aiosqlite.Connection = Depends(get_write_db),
210
+ ) -> dict:
211
+ if body.feed_url:
212
+ validate_public_url(body.feed_url)
213
+ return await update_source(
214
+ db,
215
+ user.workspace_id or "ws_default",
216
+ source_id,
217
+ name=body.name,
218
+ feed_url=body.feed_url,
219
+ active=body.active,
220
+ )
221
+
222
+
223
+ @router.delete("/sources/{source_id}", status_code=204)
224
+ async def delete_inbox_source(
225
+ source_id: str,
226
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
227
+ db: aiosqlite.Connection = Depends(get_write_db),
228
+ ) -> None:
229
+ await delete_source(db, user.workspace_id or "ws_default", source_id)
230
+
231
+
232
+ @router.get("/items", response_model=list[InboxItemSummary])
233
+ async def get_inbox_items(
234
+ needs_triage: bool = Query(
235
+ True, description="Default true: only items with status='unread'"
236
+ ),
237
+ classified: bool | None = Query(None),
238
+ source: str | None = Query(None),
239
+ program: str | None = Query(None),
240
+ topic: str | None = Query(None),
241
+ treatment: str | None = Query(None),
242
+ item_status: str | None = Query(
243
+ None, alias="status", description="Filter by status (unread, read, saved, etc.)"
244
+ ),
245
+ limit: int = Query(20, ge=1, le=200),
246
+ user: UserInfo = Depends(get_current_user_or_agent),
247
+ db: aiosqlite.Connection = Depends(get_db),
248
+ ) -> list[InboxItemSummary]:
249
+ return await list_inbox_items(
250
+ db,
251
+ user,
252
+ limit=limit,
253
+ needs_triage=needs_triage,
254
+ classified=classified,
255
+ source=source,
256
+ program=program,
257
+ topic=topic,
258
+ treatment=treatment,
259
+ status=item_status,
260
+ )
261
+
262
+
263
+ @router.get("/stats", response_model=InboxStatsResponse)
264
+ async def get_inbox_stats_route(
265
+ needs_triage: bool = Query(
266
+ True, description="Default true: only items with status='unread'"
267
+ ),
268
+ classified: bool | None = Query(None),
269
+ source: str | None = Query(None),
270
+ program: str | None = Query(None),
271
+ topic: str | None = Query(None),
272
+ treatment: str | None = Query(None),
273
+ item_status: str | None = Query(
274
+ None, alias="status", description="Filter by status (unread, read, saved, etc.)"
275
+ ),
276
+ user: UserInfo = Depends(get_current_user_or_agent),
277
+ db: aiosqlite.Connection = Depends(get_db),
278
+ ) -> InboxStatsResponse:
279
+ return await get_inbox_stats(
280
+ db,
281
+ user,
282
+ needs_triage=needs_triage,
283
+ classified=classified,
284
+ source=source,
285
+ program=program,
286
+ topic=topic,
287
+ treatment=treatment,
288
+ status=item_status,
289
+ )
290
+
291
+
292
+ @router.get("/items/unread-count")
293
+ async def get_inbox_unread_count(
294
+ user: UserInfo = Depends(get_current_user_or_agent),
295
+ db: aiosqlite.Connection = Depends(get_db),
296
+ ) -> dict:
297
+ count = await get_unread_count(db, user)
298
+ return {"count": count}
299
+
300
+
301
+ @router.get("/digest/current")
302
+ async def get_current_digest_items(
303
+ domain_key: str | None = Query(None),
304
+ limit: int = Query(200, ge=1, le=500),
305
+ user: UserInfo = Depends(get_current_user_or_agent),
306
+ db: aiosqlite.Connection = Depends(get_db),
307
+ ) -> list[dict]:
308
+ workspace_id = user.workspace_id or "ws_default"
309
+ return await list_digest_items(
310
+ db,
311
+ workspace_id,
312
+ state="visible",
313
+ domain_key=domain_key,
314
+ limit=limit,
315
+ )
316
+
317
+
318
+ @router.get("/digest/overflow")
319
+ async def get_current_digest_overflow(
320
+ domain_key: str | None = Query(None),
321
+ limit: int = Query(200, ge=1, le=500),
322
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
323
+ db: aiosqlite.Connection = Depends(get_db),
324
+ ) -> list[dict]:
325
+ workspace_id = user.workspace_id or "ws_default"
326
+ return await list_digest_items(
327
+ db,
328
+ workspace_id,
329
+ state="overflow",
330
+ domain_key=domain_key,
331
+ limit=limit,
332
+ )
333
+
334
+
335
+ @router.get("/digest/stats")
336
+ async def get_current_digest_stats(
337
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
338
+ db: aiosqlite.Connection = Depends(get_db),
339
+ ) -> dict:
340
+ workspace_id = user.workspace_id or "ws_default"
341
+ return await get_digest_stats(db, workspace_id)
342
+
343
+
344
+ @router.post("/digest/recompute")
345
+ async def recompute_digest(
346
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
347
+ ) -> dict:
348
+ workspace_id = user.workspace_id or "ws_default"
349
+ return await recompute_digest_now(workspace_id)
350
+
351
+
352
+ @router.patch("/items/{inbox_item_id}/status")
353
+ async def patch_inbox_status(
354
+ inbox_item_id: str,
355
+ body: InboxStatusUpdateRequest,
356
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
357
+ db: aiosqlite.Connection = Depends(get_write_db),
358
+ ) -> dict:
359
+ return await update_inbox_status(db, user, inbox_item_id, body)
360
+
361
+
362
+ @router.patch(
363
+ "/items/{inbox_item_id}/taxonomy", response_model=InboxTaxonomyUpdateResponse
364
+ )
365
+ async def patch_inbox_taxonomy(
366
+ inbox_item_id: str,
367
+ body: InboxTaxonomyUpdateRequest,
368
+ user: UserInfo = Depends(get_current_user_or_agent),
369
+ db: aiosqlite.Connection = Depends(get_write_db),
370
+ ) -> InboxTaxonomyUpdateResponse:
371
+ return await update_inbox_taxonomy(db, user, inbox_item_id, body)
372
+
373
+
374
+ @router.get("/items/{inbox_item_id}", response_model=InboxItemDetail)
375
+ async def get_inbox_item(
376
+ inbox_item_id: str,
377
+ user: UserInfo = Depends(get_current_user_or_agent),
378
+ db: aiosqlite.Connection = Depends(get_db),
379
+ ) -> InboxItemDetail:
380
+ return await get_inbox_item_detail(db, user, inbox_item_id)
381
+
382
+
383
+ @router.post(
384
+ "/items/{inbox_item_id}/decision", response_model=InboxTriageDecisionResponse
385
+ )
386
+ async def submit_inbox_decision(
387
+ inbox_item_id: str,
388
+ body: InboxTriageDecisionRequest,
389
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
390
+ db: aiosqlite.Connection = Depends(get_write_db),
391
+ ) -> InboxTriageDecisionResponse:
392
+ return await apply_triage_decision(db, user, inbox_item_id, body)
393
+
394
+
395
+ @router.post("/items/{inbox_item_id}/tldr")
396
+ async def generate_inbox_tldr(
397
+ inbox_item_id: str,
398
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
399
+ ) -> dict:
400
+ workspace_id = user.workspace_id or "ws_default"
401
+ return await get_or_generate_tldr(inbox_item_id, workspace_id)
402
+
403
+
404
+ @router.post("/items/{inbox_item_id}/deep-research")
405
+ async def generate_inbox_deep_research(
406
+ inbox_item_id: str,
407
+ force: bool = False,
408
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
409
+ ) -> dict:
410
+ workspace_id = user.workspace_id or "ws_default"
411
+ return await get_or_generate_deep_research(inbox_item_id, workspace_id, force=force)
412
+
413
+
414
+ class ReclassifyRequest(BaseModel):
415
+ source: str | None = Field(None, description="Filter by source name")
416
+ status: str = Field("unread", description="Filter by status (default: unread)")
417
+ limit: int = Field(100, ge=1, le=500, description="Max items to reclassify")
418
+
419
+
420
+ @router.post("/reclassify")
421
+ async def reclassify_inbox_items(
422
+ body: ReclassifyRequest,
423
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
424
+ db: aiosqlite.Connection = Depends(get_write_db),
425
+ ) -> dict:
426
+ """Queue matching inbox items for re-classification. Admin only, fire-and-forget."""
427
+ workspace_id = user.workspace_id or "ws_default"
428
+
429
+ where_clauses = ["workspace_id = ?"]
430
+ params: list[object] = [workspace_id]
431
+
432
+ if body.source:
433
+ where_clauses.append("source = ?")
434
+ params.append(body.source)
435
+ if body.status:
436
+ where_clauses.append("status = ?")
437
+ params.append(body.status)
438
+
439
+ where_sql = " AND ".join(where_clauses)
440
+ params.append(body.limit)
441
+
442
+ cursor = await db.execute(
443
+ f"SELECT id FROM inbox_items WHERE {where_sql} ORDER BY created_at DESC LIMIT ?",
444
+ tuple(params),
445
+ )
446
+ rows = await cursor.fetchall()
447
+
448
+ queued = 0
449
+ for row in rows:
450
+ try:
451
+ schedule_classification(settings.db_path, workspace_id, row["id"])
452
+ queued += 1
453
+ except Exception:
454
+ logger.exception("Failed to schedule reclassify for item %s", row["id"])
455
+
456
+ logger.info(
457
+ "Reclassify queued %d/%d items (source=%s, status=%s) by %s",
458
+ queued,
459
+ len(rows),
460
+ body.source,
461
+ body.status,
462
+ user.username,
463
+ )
464
+ return {"queued": queued}
465
+
466
+
467
+ @router.get("/source-scores")
468
+ async def get_source_scores_route(
469
+ user: UserInfo = Depends(get_current_user_or_agent),
470
+ db: aiosqlite.Connection = Depends(get_db),
471
+ ) -> list[dict]:
472
+ workspace_id = user.workspace_id or "ws_default"
473
+ return await get_source_scores(db, workspace_id)
474
+
475
+
476
+ @router.get(
477
+ "/gmail-sync/candidates",
478
+ response_model=list[InboxGmailSyncCandidate],
479
+ )
480
+ async def get_gmail_sync_candidates(
481
+ limit: int = Query(50, ge=1, le=200),
482
+ user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
483
+ db: aiosqlite.Connection = Depends(get_db),
484
+ ) -> list[InboxGmailSyncCandidate]:
485
+ workspace_id = user.workspace_id or "ws_default"
486
+ return await list_gmail_sync_candidates(db, workspace_id, limit=limit)
487
+
488
+
489
+ @router.post("/gmail-sync/{inbox_item_id}/complete")
490
+ async def complete_gmail_sync(
491
+ inbox_item_id: str,
492
+ body: InboxGmailSyncCompleteRequest,
493
+ user: UserInfo = Depends(require_agent_token_scope("inbox:ingest")),
494
+ db: aiosqlite.Connection = Depends(get_write_db),
495
+ ) -> dict:
496
+ return await mark_gmail_sync_complete(db, user, inbox_item_id, body)