android-midscene-automation 0.2.3 → 1.1.0

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 (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,564 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import sqlite3
19
+ import time
20
+ from typing import Any
21
+
22
+ from artemis.runtime import trace_store
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ try:
27
+ from admin_console.database.connection import db_session
28
+ except ImportError:
29
+ from apps.admin_console.database.connection import db_session
30
+
31
+
32
+ def _canonicalize_status(row: dict[str, Any]) -> dict[str, Any]:
33
+ """Map the legacy "success" terminal status to the canonical "completed".
34
+
35
+ Rows written before the vocabulary was unified may still carry "success";
36
+ API consumers must only ever see "completed". get_session_status is left
37
+ raw on purpose so the queue reconcile can detect and persist the rewrite.
38
+ """
39
+ if row.get("status") == "success":
40
+ row["status"] = "completed"
41
+ return row
42
+
43
+
44
+ class SessionRepository:
45
+ """Repository handling all database queries and updates for Sessions."""
46
+
47
+ def __init__(self, db_path=None):
48
+ self.db_path = db_path
49
+
50
+ @staticmethod
51
+ def process_is_alive(pid: Any) -> bool:
52
+ """Return whether a session's worker PID is still alive.
53
+
54
+ Delegates to the repository-wide probe: indeterminate liveness
55
+ (e.g. AccessDenied) counts as alive so a running session is never
56
+ reaped by mistake.
57
+ """
58
+ try:
59
+ pid_int = int(pid)
60
+ except (TypeError, ValueError):
61
+ return False
62
+ from artemis.runtime.process_probe import pid_is_alive
63
+
64
+ return pid_is_alive(pid_int)
65
+
66
+ def get_all_sessions(self) -> list[dict[str, Any]]:
67
+ with db_session(self.db_path) as conn:
68
+ cursor = conn.cursor()
69
+ cursor.execute("SELECT * FROM sessions ORDER BY start_time DESC")
70
+ return [_canonicalize_status(dict(r)) for r in cursor.fetchall()]
71
+
72
+ def get_video_recordings_map(self) -> dict[str, str]:
73
+ with db_session(self.db_path) as conn:
74
+ cursor = conn.cursor()
75
+ cursor.execute(
76
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
77
+ )
78
+ if cursor.fetchone() is None:
79
+ return {}
80
+ columns = {str(row[1]) for row in cursor.execute("PRAGMA table_info(video_recordings)")}
81
+ # Failed recordings may still be recoverable from disk.
82
+ # Callers exclude in-progress sessions before resolving paths.
83
+ ready_clause = (
84
+ "status IN ('ready', 'failed')" if "status" in columns else "end_time IS NOT NULL"
85
+ )
86
+ cursor.execute(
87
+ "SELECT session_id, local_video_path FROM video_recordings "
88
+ "WHERE session_id IS NOT NULL AND local_video_path IS NOT NULL "
89
+ f"AND {ready_clause} ORDER BY start_time ASC"
90
+ )
91
+ return {str(r[0]): str(r[1]) for r in cursor.fetchall() if r[0] and r[1]}
92
+
93
+ def get_video_recording_for_session(self, session_id: str) -> dict[str, Any] | None:
94
+ try:
95
+ with db_session(self.db_path) as conn:
96
+ cursor = conn.cursor()
97
+ cursor.execute(
98
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
99
+ )
100
+ if cursor.fetchone() is None:
101
+ return None
102
+ cursor.execute(
103
+ "SELECT * FROM video_recordings WHERE session_id = ? "
104
+ "ORDER BY start_time DESC LIMIT 1",
105
+ (str(session_id),),
106
+ )
107
+ row = cursor.fetchone()
108
+ if not row:
109
+ return None
110
+ result = dict(row)
111
+ if "status" not in result:
112
+ result["status"] = (
113
+ "ready" if result.get("end_time") is not None else "recording"
114
+ )
115
+ result.setdefault("error", None)
116
+ return result
117
+ except Exception:
118
+ return None
119
+
120
+ def get_latest_video_recordings_map(self) -> dict[str, dict[str, Any]]:
121
+ """Return the newest recording row for every session in one query.
122
+
123
+ The task-list endpoint renders every session at once. Calling
124
+ ``get_video_recording_for_session`` in that loop used to open a new
125
+ SQLite connection for every row, which made a refresh progressively
126
+ slower as history grew.
127
+ """
128
+ try:
129
+ with db_session(self.db_path) as conn:
130
+ cursor = conn.cursor()
131
+ cursor.execute(
132
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
133
+ )
134
+ if cursor.fetchone() is None:
135
+ return {}
136
+
137
+ rows = cursor.execute(
138
+ "SELECT * FROM video_recordings WHERE session_id IS NOT NULL "
139
+ "ORDER BY start_time DESC"
140
+ ).fetchall()
141
+ latest: dict[str, dict[str, Any]] = {}
142
+ for row in rows:
143
+ recording = dict(row)
144
+ session_id = str(recording.get("session_id") or "")
145
+ if not session_id or session_id in latest:
146
+ continue
147
+ if "status" not in recording:
148
+ recording["status"] = (
149
+ "ready" if recording.get("end_time") is not None else "recording"
150
+ )
151
+ recording.setdefault("error", None)
152
+ latest[session_id] = recording
153
+ return latest
154
+ except Exception:
155
+ return {}
156
+
157
+ def get_unfinalized_video_recordings(self) -> list[dict[str, Any]]:
158
+ """Recordings whose worker never finalized them, oldest first.
159
+
160
+ Rows of sessions that are still ``running`` are skipped: their worker
161
+ (possibly owned by another daemon) may still be writing the file.
162
+ """
163
+ try:
164
+ with db_session(self.db_path) as conn:
165
+ cursor = conn.cursor()
166
+ cursor.execute(
167
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
168
+ )
169
+ if cursor.fetchone() is None:
170
+ return []
171
+ columns = {
172
+ str(row[1]) for row in cursor.execute("PRAGMA table_info(video_recordings)")
173
+ }
174
+ pending_clause = (
175
+ "v.status IN ('recording', 'finalizing', 'failed')"
176
+ if "status" in columns
177
+ else "v.end_time IS NULL"
178
+ )
179
+ cursor.execute(
180
+ "SELECT v.session_id, v.local_video_path, v.start_time, "
181
+ "s.status AS session_status, s.start_time AS session_start_time "
182
+ "FROM video_recordings v LEFT JOIN sessions s ON s.session_id = v.session_id "
183
+ f"WHERE v.session_id IS NOT NULL AND v.local_video_path IS NOT NULL "
184
+ f"AND {pending_clause} ORDER BY v.start_time ASC"
185
+ )
186
+ rows = [dict(r) for r in cursor.fetchall()]
187
+ except (OSError, sqlite3.Error):
188
+ return []
189
+ return [row for row in rows if str(row.get("session_status") or "").lower() != "running"]
190
+
191
+ def mark_recording_failed_if_pending(self, session_id: str, error: str) -> bool:
192
+ """Close a recording lifecycle when its worker exits before finalization."""
193
+ try:
194
+ with db_session(self.db_path) as conn:
195
+ cursor = conn.cursor()
196
+ cursor.execute(
197
+ "UPDATE video_recordings SET status = 'failed', error = ?, "
198
+ "end_time = COALESCE(end_time, ?) "
199
+ "WHERE session_id = ? AND status IN ('recording', 'finalizing')",
200
+ (error, time.time(), str(session_id)),
201
+ )
202
+ conn.commit()
203
+ return cursor.rowcount > 0
204
+ except Exception:
205
+ return False
206
+
207
+ def mark_recording_ready(self, session_id: str, local_video_path: str) -> bool:
208
+ """Mark a recording as ready with its finalized video path."""
209
+ try:
210
+ with db_session(self.db_path) as conn:
211
+ cursor = conn.cursor()
212
+ cursor.execute(
213
+ "UPDATE video_recordings SET status = 'ready', error = NULL, "
214
+ "local_video_path = ?, end_time = COALESCE(end_time, ?) "
215
+ "WHERE session_id = ?",
216
+ (str(local_video_path), time.time(), str(session_id)),
217
+ )
218
+ updated = cursor.rowcount > 0
219
+ if os.path.exists(str(local_video_path)):
220
+ # Mirror the worker's own finalization so the session row
221
+ # and trace tooling see the recovered file too.
222
+ cursor.execute(
223
+ "UPDATE sessions SET video_filepath = ? WHERE session_id = ?",
224
+ (str(local_video_path), str(session_id)),
225
+ )
226
+ conn.commit()
227
+ return updated
228
+ except Exception:
229
+ return False
230
+
231
+ def get_llm_traces_for_profile(self, session_id: str, limit: int = 3) -> list[str]:
232
+ with db_session(self.db_path) as conn:
233
+ cursor = conn.cursor()
234
+ cursor.execute(
235
+ "SELECT payload FROM traces WHERE session_id = ? AND type = 'llm_call' LIMIT ?",
236
+ (session_id, limit),
237
+ )
238
+ rows = cursor.fetchall()
239
+ return [r[0] for r in rows if r and r[0]]
240
+
241
+ def get_llm_traces_for_profiles_map(
242
+ self, session_ids: list[str] | None = None, limit: int = 3
243
+ ) -> dict[str, list[str]]:
244
+ """Return bounded LLM payloads for only the sessions needing fallback."""
245
+ if session_ids is not None and not session_ids:
246
+ return {}
247
+
248
+ session_filter = ""
249
+ params: list[Any] = []
250
+ if session_ids is not None:
251
+ placeholders = ",".join("?" for _ in session_ids)
252
+ session_filter = f" AND session_id IN ({placeholders})"
253
+ params.extend(session_ids)
254
+ params.append(limit)
255
+
256
+ with db_session(self.db_path) as conn:
257
+ cursor = conn.cursor()
258
+ cursor.execute(
259
+ "SELECT session_id, payload FROM ("
260
+ " SELECT session_id, payload, ROW_NUMBER() OVER ("
261
+ " PARTITION BY session_id ORDER BY timestamp ASC"
262
+ " ) AS row_number FROM traces WHERE type = 'llm_call'"
263
+ f"{session_filter}"
264
+ ") WHERE row_number <= ?",
265
+ params,
266
+ )
267
+ result: dict[str, list[str]] = {}
268
+ for row in cursor.fetchall():
269
+ session_id = str(row[0] or "")
270
+ payload = row[1]
271
+ if session_id and payload:
272
+ result.setdefault(session_id, []).append(payload)
273
+ return result
274
+
275
+ def get_agent_trace_names(self, session_id: str) -> list[str]:
276
+ try:
277
+ with db_session(self.db_path) as conn:
278
+ cursor = conn.cursor()
279
+ cursor.execute(
280
+ "SELECT DISTINCT name FROM traces WHERE session_id = ? AND type IN ('agent', 'llm_call')",
281
+ (session_id,),
282
+ )
283
+ rows = cursor.fetchall()
284
+ return [r[0] for r in rows if r and r[0]]
285
+ except Exception:
286
+ return []
287
+
288
+ def get_agent_trace_names_map(self) -> dict[str, list[str]]:
289
+ """Return distinct agent/LLM trace names grouped by session."""
290
+ with db_session(self.db_path) as conn:
291
+ cursor = conn.cursor()
292
+ cursor.execute(
293
+ "SELECT session_id, name FROM traces "
294
+ "WHERE type IN ('agent', 'llm_call') AND session_id IS NOT NULL "
295
+ "GROUP BY session_id, name"
296
+ )
297
+ result: dict[str, list[str]] = {}
298
+ for row in cursor.fetchall():
299
+ session_id = str(row[0] or "")
300
+ name = row[1]
301
+ if session_id and name:
302
+ result.setdefault(session_id, []).append(name)
303
+ return result
304
+
305
+ def get_session_usage(self, session_id: str) -> dict[str, Any]:
306
+ """Aggregate measured LLM usage for one session.
307
+
308
+ Sums every ``llm_usage`` trace the token meter recorded (all agents,
309
+ Flash and Pro alike) and reports the prompt size of the latest
310
+ Operator/FlashRunner call as the live executor context. Also echoes
311
+ the profile and Pro tuning the run was started with, read from the
312
+ session's stored ``device_info``.
313
+ """
314
+ from artemis.services.token_meter import (
315
+ OPERATOR_CONTEXT_WINDOW_TOKENS,
316
+ OPERATOR_NODE_NAMES,
317
+ )
318
+
319
+ def _int(value: Any) -> int:
320
+ try:
321
+ return int(value or 0)
322
+ except (TypeError, ValueError):
323
+ return 0
324
+
325
+ llm_calls = prompt = completion = total = cached = 0
326
+ operator_context: int | None = None
327
+ operator_context_at: float | None = None
328
+ with db_session(self.db_path) as conn:
329
+ cursor = conn.cursor()
330
+ cursor.execute(
331
+ "SELECT payload, timestamp FROM traces WHERE session_id = ?"
332
+ " AND type = 'llm_call' AND name = 'llm_usage' ORDER BY timestamp ASC",
333
+ (session_id,),
334
+ )
335
+ for row in cursor.fetchall():
336
+ raw = row["payload"]
337
+ try:
338
+ payload = json.loads(raw) if isinstance(raw, str) else raw
339
+ except (ValueError, TypeError):
340
+ continue
341
+ if not isinstance(payload, dict):
342
+ continue
343
+ p = _int(payload.get("prompt_tokens"))
344
+ c = _int(payload.get("completion_tokens"))
345
+ t = _int(payload.get("total_tokens")) or (p + c)
346
+ if t <= 0:
347
+ continue
348
+ llm_calls += 1
349
+ prompt += p
350
+ completion += c
351
+ total += t
352
+ cached += _int(payload.get("cached_tokens"))
353
+ node = str(payload.get("node") or "").strip().lower()
354
+ if node in OPERATOR_NODE_NAMES:
355
+ operator_context = _int(payload.get("context_base_tokens")) or p
356
+ operator_context_at = row["timestamp"]
357
+
358
+ cursor.execute("SELECT device_info FROM sessions WHERE session_id = ?", (session_id,))
359
+ session_row = cursor.fetchone()
360
+
361
+ profile: str | None = None
362
+ run_tuning: dict[str, Any] | None = None
363
+ if session_row and session_row["device_info"]:
364
+ try:
365
+ d_info = json.loads(session_row["device_info"])
366
+ except (ValueError, TypeError):
367
+ d_info = None
368
+ if isinstance(d_info, dict):
369
+ profile = d_info.get("profile") or None
370
+ tuning = d_info.get("run_tuning")
371
+ if isinstance(tuning, dict) and tuning:
372
+ run_tuning = dict(tuning)
373
+
374
+ return {
375
+ "session_id": session_id,
376
+ "llm_calls": llm_calls,
377
+ "prompt_tokens": prompt,
378
+ "completion_tokens": completion,
379
+ "total_tokens": total,
380
+ "cached_tokens": cached,
381
+ "operator_context_tokens": operator_context,
382
+ "operator_context_window_tokens": OPERATOR_CONTEXT_WINDOW_TOKENS,
383
+ "operator_context_updated_at": operator_context_at,
384
+ "profile": profile,
385
+ "run_tuning": run_tuning,
386
+ }
387
+
388
+ def get_session_by_id(self, session_id: str) -> dict[str, Any] | None:
389
+ try:
390
+ with db_session(self.db_path) as conn:
391
+ cursor = conn.cursor()
392
+ cursor.execute(
393
+ "SELECT * FROM sessions WHERE session_id = ?",
394
+ (str(session_id),),
395
+ )
396
+ row = cursor.fetchone()
397
+ return _canonicalize_status(dict(row)) if row else None
398
+ except Exception:
399
+ return None
400
+
401
+ def harvest_orphaned_sessions(self, orphaned_ids: list[str]) -> int:
402
+ if not orphaned_ids:
403
+ return 0
404
+ with db_session(self.db_path) as conn:
405
+ cursor = conn.cursor()
406
+ now = time.time()
407
+ count = 0
408
+ for s_id in orphaned_ids:
409
+ row = cursor.execute(
410
+ "SELECT pid FROM sessions WHERE session_id = ? AND status = ?",
411
+ (s_id, "running"),
412
+ ).fetchone()
413
+ if row is None or self.process_is_alive(row["pid"]):
414
+ continue
415
+ cursor.execute(
416
+ "UPDATE sessions SET status = ?, end_time = ? "
417
+ "WHERE session_id = ? AND status = ?",
418
+ ("failed", now, s_id, "running"),
419
+ )
420
+ count += cursor.rowcount
421
+ conn.commit()
422
+ return count
423
+
424
+ def reconcile_orphaned_sessions(self) -> int:
425
+ """Mark running sessions with no live worker process as failed.
426
+
427
+ This is safe to call both during startup and immediately after a
428
+ forced server stop: live workers, including workers owned by another
429
+ server instance, are left untouched.
430
+ """
431
+ try:
432
+ with db_session(self.db_path) as conn:
433
+ cursor = conn.cursor()
434
+ rows = cursor.execute(
435
+ "SELECT session_id, pid FROM sessions WHERE status = ?", ("running",)
436
+ ).fetchall()
437
+ count = 0
438
+ now = time.time()
439
+ for row in rows:
440
+ if self.process_is_alive(row["pid"]):
441
+ continue
442
+ cursor.execute(
443
+ "UPDATE sessions SET status = ?, end_time = ? "
444
+ "WHERE session_id = ? AND status = ?",
445
+ ("failed", now, row["session_id"], "running"),
446
+ )
447
+ count += cursor.rowcount
448
+ try:
449
+ if trace_store.read_status(str(row["session_id"])):
450
+ trace_store.update_trace_status(
451
+ str(row["session_id"]),
452
+ "failed",
453
+ error="Process terminated prior to server startup.",
454
+ )
455
+ except OSError as exc:
456
+ # read_status itself never raises; this guards the
457
+ # lock/write side of update_trace_status.
458
+ logger.warning(
459
+ "Could not mark trace %s failed during orphan reconciliation: %s",
460
+ row["session_id"],
461
+ exc,
462
+ )
463
+ conn.commit()
464
+ return count
465
+ except Exception:
466
+ # Reconciliation is best-effort at startup, but a silent abort
467
+ # would leave every orphaned "running" row untouched -- log it.
468
+ logger.warning("Orphan-session reconciliation aborted", exc_info=True)
469
+ return 0
470
+
471
+ def cleanup_orphans_on_startup(self) -> int:
472
+ """Backward-compatible startup entrypoint for orphan reconciliation."""
473
+ return self.reconcile_orphaned_sessions()
474
+
475
+ def get_latest_session(self) -> dict[str, Any] | None:
476
+ try:
477
+ with db_session(self.db_path) as conn:
478
+ cursor = conn.cursor()
479
+ cursor.execute(
480
+ "SELECT session_id, status FROM sessions ORDER BY start_time DESC LIMIT 1"
481
+ )
482
+ row = cursor.fetchone()
483
+ return _canonicalize_status(dict(row)) if row else None
484
+ except Exception:
485
+ return None
486
+
487
+ def get_running_session_id(self) -> str | None:
488
+ try:
489
+ with db_session(self.db_path) as conn:
490
+ cursor = conn.cursor()
491
+ cursor.execute(
492
+ "SELECT session_id FROM sessions WHERE status = 'running' "
493
+ "ORDER BY start_time DESC LIMIT 1"
494
+ )
495
+ row = cursor.fetchone()
496
+ return row["session_id"] if row else None
497
+ except Exception:
498
+ return None
499
+
500
+ def get_session_status(self, session_id: str) -> str | None:
501
+ try:
502
+ with db_session(self.db_path) as conn:
503
+ cursor = conn.cursor()
504
+ cursor.execute(
505
+ "SELECT status FROM sessions WHERE session_id = ?",
506
+ (str(session_id),),
507
+ )
508
+ row = cursor.fetchone()
509
+ return row["status"] if row else None
510
+ except Exception:
511
+ return None
512
+
513
+ def update_session_status(
514
+ self, session_id: str, status: str, end_time: float | None = None
515
+ ) -> bool:
516
+ try:
517
+ with db_session(self.db_path) as conn:
518
+ cursor = conn.cursor()
519
+ t = end_time or time.time()
520
+ cursor.execute(
521
+ "UPDATE sessions SET status = ?, end_time = ? WHERE session_id = ?",
522
+ (status, t, str(session_id)),
523
+ )
524
+ conn.commit()
525
+ return cursor.rowcount > 0
526
+ except Exception:
527
+ return False
528
+
529
+ def mark_all_running_cancelled(self) -> int:
530
+ try:
531
+ with db_session(self.db_path) as conn:
532
+ cursor = conn.cursor()
533
+ cursor.execute(
534
+ "UPDATE sessions SET status = ?, end_time = ? WHERE status = ?",
535
+ ("cancelled", time.time(), "running"),
536
+ )
537
+ count = cursor.rowcount
538
+ conn.commit()
539
+ return count
540
+ except Exception:
541
+ return 0
542
+
543
+ def get_background_tasks(self, session_id: str) -> list[dict[str, Any]]:
544
+ try:
545
+ with db_session(self.db_path) as conn:
546
+ cursor = conn.cursor()
547
+ cursor.execute(
548
+ "SELECT * FROM background_tasks WHERE session_id = ? ORDER BY start_time ASC",
549
+ (session_id,),
550
+ )
551
+ return [dict(r) for r in cursor.fetchall()]
552
+ except Exception:
553
+ return []
554
+
555
+
556
+ session_repo = SessionRepository()
557
+
558
+ # Give the base runtime package access to orphan-session reconciliation without
559
+ # letting it import this application-layer module (inverted dependency): any
560
+ # process that loads the admin console DB layer -- the server itself, or an
561
+ # entry point stopping one -- thereby arms stop_server()'s post-kill cleanup.
562
+ from artemis.runtime.server_lifecycle import register_session_reconciler # noqa: E402
563
+
564
+ register_session_reconciler(session_repo.reconcile_orphaned_sessions)