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,548 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sqlite3
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+
10
+ from core.api.models.auth import UserInfo
11
+ from core.api.routers import agent as agent_router
12
+ from core.api.routers import sessions as sessions_router
13
+ from core.api.services import claude_metrics
14
+ from core.api.services.providers import PROVIDERS
15
+
16
+
17
+ class FakeAsyncCursor:
18
+ def __init__(self, cursor: sqlite3.Cursor) -> None:
19
+ self._cursor = cursor
20
+
21
+ async def fetchone(self):
22
+ return self._cursor.fetchone()
23
+
24
+ async def fetchall(self):
25
+ return self._cursor.fetchall()
26
+
27
+
28
+ class FakeAsyncDB:
29
+ def __init__(self, path: str) -> None:
30
+ self._conn = sqlite3.connect(path)
31
+ self._conn.row_factory = sqlite3.Row
32
+
33
+ async def execute(self, query: str, params=()):
34
+ cursor = self._conn.execute(query, params)
35
+ return FakeAsyncCursor(cursor)
36
+
37
+ async def commit(self) -> None:
38
+ self._conn.commit()
39
+
40
+ def close(self) -> None:
41
+ self._conn.close()
42
+
43
+
44
+ @pytest.fixture
45
+ def tmp_db(tmp_path: Path) -> str:
46
+ db_path = str(tmp_path / "sessions.db")
47
+ conn = sqlite3.connect(db_path)
48
+ conn.row_factory = sqlite3.Row
49
+ conn.execute(
50
+ "CREATE TABLE sessions_meta ("
51
+ "name TEXT PRIMARY KEY, "
52
+ "session_uuid TEXT, "
53
+ "created_at TEXT, "
54
+ "last_active TEXT, "
55
+ "hibernated INTEGER NOT NULL DEFAULT 0, "
56
+ "hibernated_at TEXT, "
57
+ "conversation_id TEXT, "
58
+ "provider TEXT, "
59
+ "project_slug TEXT, "
60
+ "launch_model TEXT, "
61
+ "permission_preset TEXT, "
62
+ "theme_mode TEXT, "
63
+ # Post-migration-088 schema: legacy column renamed
64
+ "last_context_pct_legacy REAL, "
65
+ # Post-migration-087 dual metrics columns (see migrations/087/088)
66
+ "last_context_pct_real REAL, "
67
+ "last_context_pct_scaled REAL, "
68
+ "last_cost_usd REAL, "
69
+ "last_cost_conversation_usd REAL, "
70
+ "last_cost_session_usd REAL, "
71
+ # Post-migration-089 shadow cost columns (PR4)
72
+ "last_cost_conversation_equivalent_usd REAL, "
73
+ "last_cost_session_equivalent_usd REAL, "
74
+ "last_cost_equivalent_pricing_version TEXT, "
75
+ "last_message_count INTEGER, "
76
+ "last_metrics_at TEXT, "
77
+ "bootstrap_message TEXT)"
78
+ )
79
+ conn.execute(
80
+ "INSERT INTO sessions_meta (name, session_uuid, created_at, hibernated, conversation_id, provider, project_slug, launch_model, permission_preset, theme_mode, bootstrap_message) "
81
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
82
+ (
83
+ "normativa",
84
+ "41f2001f-d009-4ec0-87e2-ae86be23c53b",
85
+ "2026-04-08T07:12:44+00:00",
86
+ 1,
87
+ "41f2001f-d009-4ec0-87e2-ae86be23c53b",
88
+ "claude",
89
+ "c&i-normativa",
90
+ "sonnet",
91
+ None,
92
+ None,
93
+ "carica /data/projects/c&i-normativa",
94
+ ),
95
+ )
96
+ conn.commit()
97
+ conn.close()
98
+ return db_path
99
+
100
+
101
+ def test_console_resume_uses_workspace_when_conversation_is_stored_there(
102
+ tmp_db: str,
103
+ monkeypatch: pytest.MonkeyPatch,
104
+ ):
105
+ db = FakeAsyncDB(tmp_db)
106
+ sent: list[tuple[str, str, bool]] = []
107
+
108
+ async def _session_exists(_name: str) -> bool:
109
+ return True
110
+
111
+ async def _send_keys(name: str, cmd: str, double_enter: bool = True) -> bool:
112
+ sent.append((name, cmd, double_enter))
113
+ return True
114
+
115
+ async def _noop(*_args, **_kwargs) -> bool:
116
+ return True
117
+
118
+ async def _to_thread(func, /, *args, **kwargs):
119
+ return func(*args, **kwargs)
120
+
121
+ monkeypatch.setattr(sessions_router.tmux, "session_exists", _session_exists)
122
+ monkeypatch.setattr(sessions_router.tmux, "send_keys", _send_keys)
123
+ monkeypatch.setattr(sessions_router.tmux, "exit_copy_mode", _noop)
124
+ monkeypatch.setattr(
125
+ sessions_router.tmux, "validate_session_name", lambda name: name
126
+ )
127
+ monkeypatch.setattr(sessions_router.asyncio, "to_thread", _to_thread)
128
+ monkeypatch.setattr(
129
+ sessions_router,
130
+ "resolve_project_path",
131
+ lambda _slug: "/data/projects/c&i-normativa",
132
+ )
133
+ monkeypatch.setattr(
134
+ sessions_router,
135
+ "_resolve_conversation_cwd",
136
+ lambda _conv_id, _slug: "/var/marvisx/workspace",
137
+ )
138
+
139
+ async def _run():
140
+ return await sessions_router.resume_session(
141
+ name="normativa",
142
+ _user=UserInfo(username="emilio", system_role="admin"),
143
+ db=db,
144
+ )
145
+
146
+ try:
147
+ payload = asyncio.run(_run())
148
+ finally:
149
+ db.close()
150
+
151
+ assert payload["status"] == "resumed"
152
+ assert payload["conversation_id"] == "41f2001f-d009-4ec0-87e2-ae86be23c53b"
153
+ assert len(sent) == 1
154
+ assert sent[0][0] == "normativa"
155
+ assert sent[0][2] is True
156
+ assert (
157
+ "cd /var/marvisx/workspace && claude --resume "
158
+ "41f2001f-d009-4ec0-87e2-ae86be23c53b --dangerously-skip-permissions"
159
+ ) in sent[0][1]
160
+
161
+
162
+ def test_console_resume_falls_back_to_continue_and_clears_stale_conversation(
163
+ tmp_db: str,
164
+ monkeypatch: pytest.MonkeyPatch,
165
+ ):
166
+ db = FakeAsyncDB(tmp_db)
167
+ sent: list[str] = []
168
+
169
+ async def _session_exists(_name: str) -> bool:
170
+ return True
171
+
172
+ async def _send_keys(_name: str, cmd: str, double_enter: bool = True) -> bool:
173
+ sent.append(cmd)
174
+ return True
175
+
176
+ async def _noop(*_args, **_kwargs) -> bool:
177
+ return True
178
+
179
+ async def _to_thread(func, /, *args, **kwargs):
180
+ return func(*args, **kwargs)
181
+
182
+ monkeypatch.setattr(sessions_router.tmux, "session_exists", _session_exists)
183
+ monkeypatch.setattr(sessions_router.tmux, "send_keys", _send_keys)
184
+ monkeypatch.setattr(sessions_router.tmux, "exit_copy_mode", _noop)
185
+ monkeypatch.setattr(
186
+ sessions_router.tmux, "validate_session_name", lambda name: name
187
+ )
188
+ monkeypatch.setattr(sessions_router.asyncio, "to_thread", _to_thread)
189
+ monkeypatch.setattr(
190
+ sessions_router,
191
+ "resolve_project_path",
192
+ lambda _slug: "/data/projects/c&i-normativa",
193
+ )
194
+ monkeypatch.setattr(
195
+ sessions_router, "_resolve_conversation_cwd", lambda _conv_id, _slug: None
196
+ )
197
+
198
+ async def _run():
199
+ payload = await sessions_router.resume_session(
200
+ name="normativa",
201
+ _user=UserInfo(username="emilio", system_role="admin"),
202
+ db=db,
203
+ )
204
+ cursor = await db.execute(
205
+ "SELECT conversation_id, hibernated FROM sessions_meta WHERE name = ?",
206
+ ("normativa",),
207
+ )
208
+ row = await cursor.fetchone()
209
+ return payload, row
210
+
211
+ try:
212
+ payload, row = asyncio.run(_run())
213
+ finally:
214
+ db.close()
215
+
216
+ assert payload["status"] == "resumed"
217
+ assert payload["conversation_id"] is None
218
+ assert len(sent) == 1
219
+ assert (
220
+ "cd '/data/projects/c&i-normativa' && claude --continue --dangerously-skip-permissions"
221
+ in sent[0]
222
+ )
223
+ assert row["conversation_id"] is None
224
+ assert row["hibernated"] == 0
225
+
226
+
227
+ def test_agent_resume_uses_project_aware_workspace_fallback(
228
+ tmp_db: str,
229
+ monkeypatch: pytest.MonkeyPatch,
230
+ ):
231
+ db = FakeAsyncDB(tmp_db)
232
+ sent: list[tuple[str, str, bool]] = []
233
+
234
+ async def _resolve_uuid(_uuid: str, _db) -> str:
235
+ return "normativa"
236
+
237
+ async def _send_keys(name: str, cmd: str, double_enter: bool = True) -> bool:
238
+ sent.append((name, cmd, double_enter))
239
+ return True
240
+
241
+ async def _to_thread(func, /, *args, **kwargs):
242
+ return func(*args, **kwargs)
243
+
244
+ monkeypatch.setattr(agent_router, "_resolve_uuid", _resolve_uuid)
245
+ monkeypatch.setattr(agent_router.tmux, "send_keys", _send_keys)
246
+ monkeypatch.setattr(agent_router.tmux, "validate_session_name", lambda name: name)
247
+ monkeypatch.setattr(agent_router, "_log_action", lambda *args, **kwargs: None)
248
+ monkeypatch.setattr(agent_router.asyncio, "to_thread", _to_thread)
249
+ monkeypatch.setattr(
250
+ agent_router,
251
+ "resolve_project_path",
252
+ lambda _slug: "/data/projects/c&i-normativa",
253
+ )
254
+ monkeypatch.setattr(
255
+ claude_metrics,
256
+ "find_conversation_cwd",
257
+ lambda _conv_id, _paths: "/var/marvisx/workspace",
258
+ )
259
+
260
+ async def _run():
261
+ return await agent_router.resume_session(
262
+ uuid="41f2001f-d009-4ec0-87e2-ae86be23c53b",
263
+ user=UserInfo(
264
+ username="agent:rem", system_role="operator", user_type="agent"
265
+ ),
266
+ db=db,
267
+ )
268
+
269
+ try:
270
+ payload = asyncio.run(_run())
271
+ finally:
272
+ db.close()
273
+
274
+ assert payload["status"] == "resumed"
275
+ assert payload["conversation_id"] == "41f2001f-d009-4ec0-87e2-ae86be23c53b"
276
+ assert len(sent) == 1
277
+ assert sent[0][2] is True
278
+ assert (
279
+ "cd /var/marvisx/workspace && claude --resume "
280
+ "41f2001f-d009-4ec0-87e2-ae86be23c53b --dangerously-skip-permissions"
281
+ ) in sent[0][1]
282
+
283
+
284
+ def test_codex_provider_uses_yolo_full_access():
285
+ assert PROVIDERS["codex"].cli_flags == "--dangerously-bypass-approvals-and-sandbox"
286
+ assert claude_metrics.normalize_cwd("~/workspace").endswith("/workspace")
287
+
288
+
289
+ def test_console_restart_recreates_non_claude_tmux_session(
290
+ tmp_db: str,
291
+ monkeypatch: pytest.MonkeyPatch,
292
+ ):
293
+ db = FakeAsyncDB(tmp_db)
294
+ sent_raw: list[str] = []
295
+ created: list[tuple[str, str | None]] = []
296
+ killed: list[str] = []
297
+
298
+ conn = sqlite3.connect(tmp_db)
299
+ conn.execute(
300
+ "INSERT INTO sessions_meta (name, session_uuid, hibernated, conversation_id, provider, project_slug, launch_model, permission_preset, theme_mode, bootstrap_message) "
301
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
302
+ (
303
+ "opencode-test",
304
+ "c6d113bc-8f6e-4d76-a8d5-b1b64e701111",
305
+ 0,
306
+ None,
307
+ "opencode",
308
+ "marvisx",
309
+ "openai/gpt-5.4",
310
+ "yolo",
311
+ "dark",
312
+ None,
313
+ ),
314
+ )
315
+ conn.commit()
316
+ conn.close()
317
+
318
+ async def _session_exists(_name: str) -> bool:
319
+ return True
320
+
321
+ async def _get_session_status(_name: str) -> str:
322
+ return "opencode"
323
+
324
+ async def _send_keys_raw(_name: str, *keys: str) -> bool:
325
+ sent_raw.extend(keys)
326
+ return True
327
+
328
+ async def _kill_session(name: str) -> bool:
329
+ killed.append(name)
330
+ return True
331
+
332
+ async def _create_session(
333
+ name: str, start_dir: str = "", start_command: str | None = None
334
+ ) -> bool:
335
+ _ = start_dir
336
+ created.append((name, start_command))
337
+ return True
338
+
339
+ async def _fail_send_keys(*_args, **_kwargs):
340
+ raise AssertionError(
341
+ "restart should recreate tmux session instead of injecting into existing pane"
342
+ )
343
+
344
+ async def _noop(*_args, **_kwargs):
345
+ return True
346
+
347
+ async def _sleep(_seconds: float):
348
+ return None
349
+
350
+ monkeypatch.setattr(sessions_router.tmux, "session_exists", _session_exists)
351
+ monkeypatch.setattr(sessions_router.tmux, "get_session_status", _get_session_status)
352
+ monkeypatch.setattr(sessions_router.tmux, "send_keys_raw", _send_keys_raw)
353
+ monkeypatch.setattr(sessions_router.tmux, "kill_session", _kill_session)
354
+ monkeypatch.setattr(sessions_router.tmux, "create_session", _create_session)
355
+ monkeypatch.setattr(sessions_router.tmux, "send_keys", _fail_send_keys)
356
+ monkeypatch.setattr(sessions_router.tmux, "exit_copy_mode", _noop)
357
+ monkeypatch.setattr(
358
+ sessions_router.tmux, "validate_session_name", lambda name: name
359
+ )
360
+ monkeypatch.setattr(
361
+ sessions_router,
362
+ "build_session_start_spec",
363
+ lambda *_args, **_kwargs: SimpleNamespace(
364
+ start_command="opencode start --fresh",
365
+ launch_dir="/var/marvisx/workspace",
366
+ ),
367
+ )
368
+ monkeypatch.setattr(sessions_router.asyncio, "sleep", _sleep)
369
+
370
+ async def _run():
371
+ return await sessions_router.restart_session(
372
+ name="opencode-test",
373
+ _user=UserInfo(username="emilio", system_role="admin"),
374
+ db=db,
375
+ )
376
+
377
+ try:
378
+ payload = asyncio.run(_run())
379
+ finally:
380
+ db.close()
381
+
382
+ assert payload["status"] == "restarted"
383
+ assert payload["resumed"] is False
384
+ assert sent_raw == ["C-c", "/exit", "Enter"]
385
+ assert killed == ["opencode-test"]
386
+ assert created == [("opencode-test", "opencode start --fresh")]
387
+
388
+
389
+ def test_console_resume_reuses_stored_opencode_session_id(
390
+ tmp_db: str,
391
+ monkeypatch: pytest.MonkeyPatch,
392
+ ):
393
+ db = FakeAsyncDB(tmp_db)
394
+ recreated: list[tuple[str, str]] = []
395
+
396
+ conn = sqlite3.connect(tmp_db)
397
+ conn.execute(
398
+ "INSERT INTO sessions_meta (name, session_uuid, created_at, hibernated, conversation_id, provider, project_slug, launch_model, permission_preset, theme_mode, bootstrap_message) "
399
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
400
+ (
401
+ "opencode-resume",
402
+ "3cf7f6ef-0000-4d76-a8d5-b1b64e701111",
403
+ "2026-04-08T07:12:44+00:00",
404
+ 1,
405
+ "ses_296333d27ffeWpQ1ckV3VVikAy",
406
+ "opencode",
407
+ "marvisx",
408
+ "openai/gpt-5.4",
409
+ "yolo",
410
+ "light",
411
+ None,
412
+ ),
413
+ )
414
+ conn.commit()
415
+ conn.close()
416
+
417
+ async def _session_exists(_name: str) -> bool:
418
+ return True
419
+
420
+ async def _noop(*_args, **_kwargs):
421
+ return True
422
+
423
+ async def _recreate(name: str, cmd: str) -> None:
424
+ recreated.append((name, cmd))
425
+
426
+ monkeypatch.setattr(sessions_router.tmux, "session_exists", _session_exists)
427
+ monkeypatch.setattr(sessions_router.tmux, "exit_copy_mode", _noop)
428
+ monkeypatch.setattr(
429
+ sessions_router.tmux, "validate_session_name", lambda name: name
430
+ )
431
+ monkeypatch.setattr(sessions_router, "_recreate_tmux_session", _recreate)
432
+
433
+ async def _run():
434
+ return await sessions_router.resume_session(
435
+ name="opencode-resume",
436
+ _user=UserInfo(username="emilio", system_role="admin"),
437
+ db=db,
438
+ )
439
+
440
+ try:
441
+ payload = asyncio.run(_run())
442
+ finally:
443
+ db.close()
444
+
445
+ assert payload["status"] == "resumed"
446
+ assert payload["conversation_id"] == "ses_296333d27ffeWpQ1ckV3VVikAy"
447
+ assert recreated[0][0] == "opencode-resume"
448
+ assert "--session ses_296333d27ffeWpQ1ckV3VVikAy" in recreated[0][1]
449
+ assert "--model openai/gpt-5.4" in recreated[0][1]
450
+ assert '"permission":"allow"' in recreated[0][1]
451
+ assert "tui.light.json" in recreated[0][1]
452
+
453
+
454
+ def test_console_restart_backfills_opencode_session_id_from_created_at(
455
+ tmp_db: str,
456
+ monkeypatch: pytest.MonkeyPatch,
457
+ ):
458
+ db = FakeAsyncDB(tmp_db)
459
+ sent_raw: list[str] = []
460
+ recreated: list[tuple[str, str]] = []
461
+
462
+ conn = sqlite3.connect(tmp_db)
463
+ conn.execute(
464
+ "INSERT INTO sessions_meta (name, session_uuid, created_at, hibernated, conversation_id, provider, project_slug, launch_model, permission_preset, theme_mode, bootstrap_message) "
465
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
466
+ (
467
+ "opencode-backfill",
468
+ "4cf7f6ef-0000-4d76-a8d5-b1b64e701111",
469
+ "2026-04-08T07:12:44+00:00",
470
+ 0,
471
+ None,
472
+ "opencode",
473
+ "marvisx",
474
+ "openai/gpt-5.4",
475
+ "yolo",
476
+ "dark",
477
+ None,
478
+ ),
479
+ )
480
+ conn.commit()
481
+ conn.close()
482
+
483
+ async def _session_exists(_name: str) -> bool:
484
+ return True
485
+
486
+ async def _get_session_status(_name: str) -> str:
487
+ return "opencode"
488
+
489
+ async def _send_keys_raw(_name: str, *keys: str) -> bool:
490
+ sent_raw.extend(keys)
491
+ return True
492
+
493
+ async def _noop(*_args, **_kwargs):
494
+ return True
495
+
496
+ async def _sleep(_seconds: float):
497
+ return None
498
+
499
+ async def _recreate(name: str, cmd: str) -> None:
500
+ recreated.append((name, cmd))
501
+
502
+ async def _to_thread(func, /, *args, **kwargs):
503
+ return func(*args, **kwargs)
504
+
505
+ monkeypatch.setattr(sessions_router.tmux, "session_exists", _session_exists)
506
+ monkeypatch.setattr(sessions_router.tmux, "get_session_status", _get_session_status)
507
+ monkeypatch.setattr(sessions_router.tmux, "send_keys_raw", _send_keys_raw)
508
+ monkeypatch.setattr(sessions_router.tmux, "exit_copy_mode", _noop)
509
+ monkeypatch.setattr(
510
+ sessions_router.tmux, "validate_session_name", lambda name: name
511
+ )
512
+ monkeypatch.setattr(sessions_router, "_recreate_tmux_session", _recreate)
513
+ monkeypatch.setattr(sessions_router.asyncio, "sleep", _sleep)
514
+ monkeypatch.setattr(sessions_router.asyncio, "to_thread", _to_thread)
515
+ monkeypatch.setattr(
516
+ sessions_router.opencode_sessions,
517
+ "find_session_id_for_created_at",
518
+ lambda *_args, **_kwargs: "ses_backfilled123",
519
+ )
520
+
521
+ async def _run():
522
+ payload = await sessions_router.restart_session(
523
+ name="opencode-backfill",
524
+ _user=UserInfo(username="emilio", system_role="admin"),
525
+ db=db,
526
+ )
527
+ cursor = await db.execute(
528
+ "SELECT conversation_id FROM sessions_meta WHERE name = ?",
529
+ ("opencode-backfill",),
530
+ )
531
+ row = await cursor.fetchone()
532
+ return payload, row["conversation_id"]
533
+
534
+ try:
535
+ payload, stored_id = asyncio.run(_run())
536
+ finally:
537
+ db.close()
538
+
539
+ assert payload["status"] == "restarted"
540
+ assert payload["resumed"] is True
541
+ assert payload["previous_conversation_id"] == "ses_backfilled123"
542
+ assert stored_id == "ses_backfilled123"
543
+ assert sent_raw == ["C-c", "/exit", "Enter"]
544
+ assert recreated[0][0] == "opencode-backfill"
545
+ assert "--session ses_backfilled123" in recreated[0][1]
546
+ assert "--model openai/gpt-5.4" in recreated[0][1]
547
+ assert '"permission":"allow"' in recreated[0][1]
548
+ assert "tui.dark.json" in recreated[0][1]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+
5
+ from core.api import db as db_module
6
+ from core.api.db import _add_session_theme_mode_column
7
+
8
+
9
+ def test_add_session_theme_mode_column_is_idempotent() -> None:
10
+ conn = sqlite3.connect(":memory:")
11
+ try:
12
+ conn.execute("CREATE TABLE sessions_meta (name TEXT PRIMARY KEY)")
13
+
14
+ _add_session_theme_mode_column(conn)
15
+ _add_session_theme_mode_column(conn)
16
+
17
+ columns = [row[1] for row in conn.execute("PRAGMA table_info(sessions_meta)")]
18
+ assert "theme_mode" in columns
19
+ assert columns.count("theme_mode") == 1
20
+ finally:
21
+ conn.close()
22
+
23
+
24
+ def test_migration_057_marks_schema_current_when_column_already_exists(
25
+ tmp_path,
26
+ monkeypatch,
27
+ ) -> None:
28
+ db_path = tmp_path / "migration-057-existing-column.db"
29
+ conn = sqlite3.connect(db_path)
30
+ try:
31
+ conn.execute(
32
+ "CREATE TABLE schema_versions (version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
33
+ )
34
+ conn.execute(
35
+ "CREATE TABLE sessions_meta (name TEXT PRIMARY KEY, theme_mode TEXT DEFAULT NULL)"
36
+ )
37
+ conn.execute("INSERT INTO schema_versions (version) VALUES (56)")
38
+ conn.commit()
39
+ finally:
40
+ conn.close()
41
+
42
+ monkeypatch.setattr(db_module.settings, "db_path", str(db_path))
43
+
44
+ db_module.run_migrations()
45
+
46
+ conn = sqlite3.connect(db_path)
47
+ try:
48
+ columns = [row[1] for row in conn.execute("PRAGMA table_info(sessions_meta)")]
49
+ latest_version = conn.execute(
50
+ "SELECT version FROM schema_versions ORDER BY version DESC LIMIT 1"
51
+ ).fetchone()[0]
52
+ finally:
53
+ conn.close()
54
+
55
+ assert columns.count("theme_mode") == 1
56
+ assert latest_version == 57