mobile-debug-mcp 0.34.0 → 0.36.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 (500) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,11 @@
1
+ /** Fields consumed from persisted NDJSON log events; other fields pass through. */
2
+ export interface StreamLogEntry {
3
+ timestamp?: string | number | null
4
+ _iso?: string | null
5
+ level?: string
6
+ message?: string
7
+ exception?: string
8
+ crash?: boolean
9
+ }
10
+
11
+ export interface LogFilters { appId?: string; deviceId?: string; pid?: number; tag?: string; level?: string; contains?: string; since_seconds?: number; limit?: number }
@@ -1,5 +1,5 @@
1
- import { execAdb, parseLogLine } from '../utils/android/utils.js'
2
- import { execCommand } from '../utils/ios/utils.js'
1
+ import { execAdb, parseLogLine } from '../../utils/android/utils.js'
2
+ import { execCommand } from '../../utils/ios/utils.js'
3
3
 
4
4
  export type NetworkErrorCode =
5
5
  | 'timeout'
@@ -35,14 +35,14 @@ export async function getAppReadiness(args: Target & { timeoutMs?: number; stabi
35
35
  let count = 0
36
36
  while (wait || count < 2) {
37
37
  remainingBudget()
38
- debugPause = device!.platform === 'android' ? pausedReadiness(device!.id) : null
38
+ debugPause = pausedReadiness(device!.id, device!.platform)
39
39
  if (debugPause) { failureCode = wait ? 'DEBUGGER_PAUSED' : undefined; return }
40
40
  const captureStarted = Date.now()
41
41
  try {
42
42
  const tree = await ToolsObserve.getUITreeHandler({ platform: device!.platform as 'android' | 'ios', deviceId: device!.id })
43
43
  remainingBudget()
44
44
  tracker.observe(tree, captureStarted)
45
- if (isDeviceUnavailable((tree as any)?.error)) { failureCode = 'DEVICE_UNAVAILABLE'; return }
45
+ if (isDeviceUnavailable(tree?.error)) { failureCode = 'DEVICE_UNAVAILABLE'; return }
46
46
  } catch (error) {
47
47
  if (/ACTION_TIMEOUT/.test(String(error))) throw error
48
48
  tracker.unavailable()
@@ -64,8 +64,10 @@ export async function getAppReadiness(args: Target & { timeoutMs?: number; stabi
64
64
  if (debugPause) return { success: !wait, device, readiness: debugPause, elapsed_ms: performance.now() - started, ...(wait ? { failure_code: 'DEBUGGER_PAUSED' } : {}) }
65
65
  const state = tracker.summary.state
66
66
  const success = !failureCode && (wait ? state === 'settled' : state !== 'unknown')
67
- return { success, device, readiness: tracker.summary, elapsed_ms: performance.now() - started,
68
- ...(!success ? { failure_code: failureCode ?? (wait ? 'READINESS_TIMEOUT' : 'READINESS_UNAVAILABLE') } : {}) }
67
+ return {
68
+ success, device, readiness: tracker.summary, elapsed_ms: performance.now() - started,
69
+ ...(!success ? { failure_code: failureCode ?? (wait ? 'READINESS_TIMEOUT' : 'READINESS_UNAVAILABLE') } : {})
70
+ }
69
71
  }
70
72
 
71
73
  export async function withReadyWait<T extends object | undefined, F extends object>(target: Target, timeoutMs: number, operation: (device: DeviceInfo) => Promise<T>, failure: (code: string, elapsedMs: number, samples: number) => F) {
@@ -1,3 +1,4 @@
1
+ import type { GetUITreeResponse } from '../types.js'
1
2
  import { pausedReadiness } from '../debugger/guard.js'
2
3
  import { AsyncLocalStorage } from 'node:async_hooks'
3
4
  import type { ReadinessSummary } from '../types.js'
@@ -17,19 +18,21 @@ export class ReadinessTracker {
17
18
  summary: ReadinessSummary
18
19
 
19
20
  constructor(private platform: string, private deviceId: string, private stabilityMs = 500) {
20
- this.summary = {
21
- state: 'unknown', scope: 'device_ui', basis: 'insufficient', captured_at_ms: null,
22
- sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
23
- evidence: [], limitations: [...baselineLimitations, this.platform === 'android' ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', 'INSUFFICIENT_SAMPLES'], capabilities: { ...capabilities, debugger_paused: this.platform === 'android' }
24
- }
21
+ this.summary = {
22
+ state: 'unknown', scope: 'device_ui', basis: 'insufficient', captured_at_ms: null,
23
+ sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
24
+ evidence: [], limitations: [...baselineLimitations, ['android', 'ios'].includes(this.platform) ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', 'INSUFFICIENT_SAMPLES'], capabilities: { ...capabilities, debugger_paused: ['android', 'ios'].includes(this.platform) }
25
+ }
25
26
 
26
27
  }
27
28
 
28
29
  unavailable(code = 'OBSERVATION_FAILED'): ReadinessSummary {
29
30
  this.signature = null
30
- this.summary = { ...this.summary, state: 'unknown', basis: 'insufficient', stable_for_ms: 0,
31
- sample_count: this.summary.sample_count + 1, limitations: [...baselineLimitations, this.platform === 'android' ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', code],
32
- evidence: [...this.summary.evidence, { code, source: 'ui_tree', captured_at_ms: null, snapshot_revision: null }].slice(-8) }
31
+ this.summary = {
32
+ ...this.summary, state: 'unknown', basis: 'insufficient', stable_for_ms: 0,
33
+ sample_count: this.summary.sample_count + 1, limitations: [...baselineLimitations, ['android', 'ios'].includes(this.platform) ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', code],
34
+ evidence: [...this.summary.evidence, { code, source: 'ui_tree', captured_at_ms: null, snapshot_revision: null }].slice(-8)
35
+ }
33
36
  return this.summary
34
37
  }
35
38
 
@@ -38,38 +41,49 @@ export class ReadinessTracker {
38
41
  return this.summary
39
42
  }
40
43
 
41
- observe(tree: any, acquisitionStartedAtMs: number, now = performance.now()): ReadinessSummary {
42
- const paused = this.platform === 'android' ? pausedReadiness(this.deviceId) : null
44
+ observe(sample: unknown, acquisitionStartedAtMs: number, now = performance.now()): ReadinessSummary {
45
+ const paused = pausedReadiness(this.deviceId, this.platform)
43
46
  if (paused) { this.summary = paused; return this.summary }
47
+ const tree = sample as GetUITreeResponse | null | undefined
44
48
  if (tree?.error) return this.unavailable('OBSERVATION_FAILED')
45
- if (!tree || tree.device?.platform !== this.platform || tree.device?.id !== this.deviceId ||
46
- tree.snapshot_session_id !== getSnapshotSessionId() || !Number.isSafeInteger(tree.snapshot_revision) || tree.snapshot_revision <= 0 ||
47
- !Number.isFinite(tree.captured_at_ms) || tree.captured_at_ms < acquisitionStartedAtMs || tree.captured_at_ms <= 0 ||
48
- !Array.isArray(tree.elements) || !tree.elements.length || tree.elements.some((element: unknown) => !element || typeof element !== 'object') ||
49
- !Number.isFinite(tree.resolution?.width) || !Number.isFinite(tree.resolution?.height) || tree.resolution.width <= 0 || tree.resolution.height <= 0) return this.unavailable('OBSERVATION_INVALID')
49
+ if (!validReadinessSample(tree, this.platform, this.deviceId, acquisitionStartedAtMs)) return this.unavailable('OBSERVATION_INVALID')
50
50
  const signature = computeSnapshotSignature(tree)
51
51
  if (!signature) return this.unavailable('OBSERVATION_INVALID')
52
- const nativeLoading = tree.elements.some((element: any) => element.visible === true && ['android.widget.ProgressBar', 'ActivityIndicator', 'ProgressIndicator'].includes(element.type))
53
- const textLoading = tree.elements.some((element: any) => element.visible === true && [element.text, element.contentDescription].some(label =>
52
+ const nativeLoading = tree.elements.some((element) => element.visible === true && ['android.widget.ProgressBar', 'ActivityIndicator', 'ProgressIndicator'].includes(element.type))
53
+ const textLoading = tree.elements.some((element) => element.visible === true && [element.text, element.contentDescription].some(label =>
54
54
  typeof label === 'string' && /^(loading|please\s+wait)[.\s…]*$/i.test(label.trim())))
55
55
  const previous = this.signature
56
56
  const unchanged = previous === signature
57
57
  if (!unchanged || nativeLoading || textLoading) this.stableSince = now
58
58
  this.signature = signature
59
59
  const stableFor = unchanged && !nativeLoading && !textLoading ? Math.max(0, now - this.stableSince) : 0
60
- const state = nativeLoading || textLoading ? 'loading' : !previous ? 'unknown' : !unchanged ? 'transitioning' : stableFor >= this.stabilityMs ? 'settled' : 'unknown'
61
- const code = nativeLoading ? 'NATIVE_LOADING_INDICATOR' : textLoading ? 'LOADING_LABEL' : state === 'transitioning' ? 'SNAPSHOT_CHANGED' : state === 'settled' ? 'SNAPSHOT_STABLE' : 'INSUFFICIENT_SAMPLES'
60
+ const state = readinessState(nativeLoading || textLoading, previous, unchanged, stableFor, this.stabilityMs)
61
+ const code = readinessCode(nativeLoading, textLoading, state)
62
62
  this.summary = {
63
63
  state, scope: 'device_ui', basis: textLoading && !nativeLoading ? 'heuristic' : state === 'unknown' ? 'insufficient' : 'observed',
64
64
  captured_at_ms: tree.captured_at_ms, snapshot_session_id: tree.snapshot_session_id, snapshot_revision: tree.snapshot_revision,
65
65
  sample_count: this.summary.sample_count + 1, stable_for_ms: stableFor,
66
66
  evidence: [...this.summary.evidence, { code, source: 'ui_tree', captured_at_ms: tree.captured_at_ms, snapshot_revision: tree.snapshot_revision }].slice(-8),
67
- limitations: [...baselineLimitations, this.platform === 'android' ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', ...(state === 'unknown' ? ['INSUFFICIENT_SAMPLES'] : [])], capabilities: { ...capabilities, debugger_paused: this.platform === 'android' }
67
+ limitations: [...baselineLimitations, ['android', 'ios'].includes(this.platform) ? 'MANAGED_DEBUGGER_ONLY' : 'DEBUGGER_SIGNAL_UNSUPPORTED', ...(state === 'unknown' ? ['INSUFFICIENT_SAMPLES'] : [])], capabilities: { ...capabilities, debugger_paused: ['android', 'ios'].includes(this.platform) }
68
68
  }
69
69
  return this.summary
70
70
  }
71
71
  }
72
72
 
73
+ function validReadinessSample(tree: GetUITreeResponse | null | undefined, platform: string, deviceId: string, acquisitionStartedAtMs: number): tree is GetUITreeResponse & { snapshot_session_id: string } {
74
+ return !!tree && validIdentity(tree, platform, deviceId) && validFreshness(tree, acquisitionStartedAtMs) && validGeometry(tree)
75
+ }
76
+ function validIdentity(tree: GetUITreeResponse, platform: string, deviceId: string) {
77
+ return tree.device?.platform === platform && tree.device?.id === deviceId &&
78
+ tree.snapshot_session_id === getSnapshotSessionId() && Number.isSafeInteger(tree.snapshot_revision) && tree.snapshot_revision > 0
79
+ }
80
+ function validFreshness(tree: GetUITreeResponse, startedAtMs: number) {
81
+ return Number.isFinite(tree.captured_at_ms) && !(tree.captured_at_ms < startedAtMs) && tree.captured_at_ms > 0
82
+ }
83
+ function validGeometry(tree: GetUITreeResponse) {
84
+ return Array.isArray(tree.elements) && tree.elements.length > 0 && !tree.elements.some((element: unknown) => !element || typeof element !== 'object') &&
85
+ Number.isFinite(tree.resolution?.width) && Number.isFinite(tree.resolution?.height) && tree.resolution.width > 0 && tree.resolution.height > 0
86
+ }
73
87
  const waits = new AsyncLocalStorage<ReadinessTracker>()
74
88
  export function currentReadinessWait() { return waits.getStore() }
75
89
  export function withReadinessTracker<T>(tracker: ReadinessTracker, operation: () => Promise<T>) { return waits.run(tracker, operation) }
@@ -79,6 +93,13 @@ export function observeReadinessWait(tree: unknown, startedAtMs: number) {
79
93
  if (!tracker) return
80
94
  remainingBudget()
81
95
  const summary = tracker.observe(tree, startedAtMs)
82
- if (isDeviceUnavailable((tree as any)?.error)) throw new Error('READINESS_DEVICE_UNAVAILABLE')
96
+ if (isDeviceUnavailable((tree as { error?: unknown } | null)?.error)) throw new Error('READINESS_DEVICE_UNAVAILABLE')
83
97
  if (summary.limitations.some(code => ['OBSERVATION_INVALID', 'OBSERVATION_FAILED'].includes(code))) throw new Error('READINESS_SAMPLE_INVALID')
84
98
  }
99
+
100
+ function readinessState(loading: boolean, previous: string | null, unchanged: boolean, stableFor: number, stabilityMs: number): ReadinessSummary['state'] {
101
+ return loading ? 'loading' : !previous ? 'unknown' : !unchanged ? 'transitioning' : stableFor >= stabilityMs ? 'settled' : 'unknown'
102
+ }
103
+ function readinessCode(nativeLoading: boolean, textLoading: boolean, state: ReadinessSummary['state']) {
104
+ return nativeLoading ? 'NATIVE_LOADING_INDICATOR' : textLoading ? 'LOADING_LABEL' : state === 'transitioning' ? 'SNAPSHOT_CHANGED' : state === 'settled' ? 'SNAPSHOT_STABLE' : 'INSUFFICIENT_SAMPLES'
105
+ }
@@ -0,0 +1,32 @@
1
+ import type { Metadata } from 'sharp'
2
+ interface EncodedScreenshot { screenshot: string; screenshot_mime: string; screenshot_fallback?: string | null; screenshot_fallback_mime?: string }
3
+ async function convert(buffer: Buffer, platform: 'android' | 'ios') {
4
+ const { default: sharp } = await import('sharp')
5
+ const image = sharp(buffer)
6
+ const meta = await image.metadata().catch((error: unknown) => {
7
+ console.error(platform === 'android' ? 'sharp.metadata failed (Android):' : 'sharp.metadata failed:', error)
8
+ return {} as Metadata
9
+ })
10
+ const hasAlpha = !!meta.hasAlpha || !!(meta.channels && meta.channels > 3)
11
+ let webp: Buffer | null = null, jpeg: Buffer | null = null
12
+ try { webp = await image.webp({ quality: 80 }).toBuffer() }
13
+ catch (error) { console.error(`WebP conversion failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error)) }
14
+ try { jpeg = await image.jpeg({ quality: 80 }).toBuffer() }
15
+ catch (error) { console.error(`JPEG conversion failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error)) }
16
+ return { hasAlpha, webp, jpeg }
17
+ }
18
+ export async function encodeScreenshot(buffer: Buffer, platform: 'android' | 'ios'): Promise<EncodedScreenshot> {
19
+ const png = buffer.toString('base64')
20
+ try {
21
+ const { hasAlpha, webp, jpeg } = await convert(buffer, platform)
22
+ if (webp) {
23
+ if (hasAlpha && platform === 'ios') return { screenshot: webp.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: png, screenshot_fallback_mime: 'image/png' }
24
+ const fallback = jpeg ? jpeg.toString('base64') : platform === 'android' ? null : undefined
25
+ return { screenshot: webp.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: fallback, screenshot_fallback_mime: jpeg ? 'image/jpeg' : undefined }
26
+ }
27
+ if (!hasAlpha && jpeg) return { screenshot: jpeg.toString('base64'), screenshot_mime: 'image/jpeg' }
28
+ } catch (error) {
29
+ console.error(`Screenshot conversion pipeline failed (${platform === 'android' ? 'Android' : 'iOS'}):`, error instanceof Error ? error.message : String(error))
30
+ }
31
+ return { screenshot: png, screenshot_mime: 'image/png' }
32
+ }
@@ -0,0 +1,69 @@
1
+ import type { CaptureDebugSnapshotRawResponse } from '../types.js'
2
+ import type { ToolsObserve } from './index.js'
3
+ import { omitEvidence } from './evidence.js'
4
+ import { measure } from '../utils/timing.js'
5
+ import { deriveSnapshotMetadata } from './snapshot-metadata.js'
6
+ import { deriveSnapshotSemantic } from './snapshot-semantic.js'
7
+ import { snapshotLog, streamLog, type SnapshotLogEntry } from './snapshot-logs.js'
8
+ export interface SnapshotInput { reason?: string; includeLogs?: boolean; includeScreenshot?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string }
9
+ type Dependencies = Pick<typeof ToolsObserve, 'captureScreenshotHandler' | 'getCurrentScreenHandler' | 'getScreenFingerprintHandler' | 'getUITreeHandler' | 'readLogStreamHandler' | 'getLogsHandler'>
10
+ async function settle<T extends Record<string, Promise<unknown>>>(tasks: T) {
11
+ const results = await Promise.allSettled(Object.values(tasks))
12
+ return Object.fromEntries(Object.keys(tasks).map((key, index) => [key, results[index]])) as { [K in keyof T]: PromiseSettledResult<Awaited<T[K]>> }
13
+ }
14
+ function apply<T>(result: PromiseSettledResult<T>, success: (value: T) => void, failure: (message: string) => void) {
15
+ if (result.status === 'fulfilled') success(result.value)
16
+ else failure(result.reason instanceof Error ? result.reason.message : String(result.reason))
17
+ }
18
+ async function collect(raw: CaptureDebugSnapshotRawResponse, input: SnapshotInput, deps: Dependencies) {
19
+ const { platform, deviceId, includeScreenshot = true, includeLogs = true, sessionId, logLines = 200 } = input
20
+ const results = await settle({
21
+ screenshot: includeScreenshot ? measure('screenshot_ms', () => deps.captureScreenshotHandler({ platform, deviceId })) : Promise.resolve(null),
22
+ currentScreen: !platform || platform === 'android' ? deps.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
23
+ fingerprint: deps.getScreenFingerprintHandler({ platform, deviceId }),
24
+ uiTree: deps.getUITreeHandler({ platform, deviceId }),
25
+ readLogStream: includeLogs ? deps.readLogStreamHandler({ platform, deviceId, sessionId: sessionId || 'default', limit: logLines }) : Promise.resolve({ entries: [] })
26
+ })
27
+ let entries: unknown[] = []
28
+ apply(results.screenshot, value => { raw.screenshot = value && value.screenshot ? value.screenshot : null }, error => { raw.screenshot_error = error })
29
+ apply(results.currentScreen, value => { raw.activity = value && (value.activity || value.shortActivity) ? value.activity || value.shortActivity : raw.activity || '' }, error => { raw.activity_error = error })
30
+ apply(results.fingerprint, value => {
31
+ if (value && value.fingerprint) raw.fingerprint = value.fingerprint
32
+ if (value && value.activity) raw.activity = raw.activity || value.activity
33
+ if (value && value.error) raw.fingerprint_error = value.error
34
+ }, error => { raw.fingerprint = null; raw.fingerprint_error = error })
35
+ apply(results.uiTree, value => { raw.ui_tree = value; if (value && value.error) raw.ui_tree_error = value.error }, error => { raw.ui_tree = null; raw.ui_tree_error = error })
36
+ apply(results.readLogStream, value => { entries = Array.isArray(value?.entries) ? value.entries : [] }, error => { raw.logs_error = error })
37
+ return entries
38
+ }
39
+ async function collectLogs(entries: unknown[], input: SnapshotInput, deps: Dependencies): Promise<SnapshotLogEntry[]> {
40
+ if (entries.length) return entries.map(streamLog)
41
+ const { platform, appId, deviceId, logLines = 200 } = input
42
+ const response = await deps.getLogsHandler({ platform, appId, deviceId, lines: logLines })
43
+ return (response && response.logs ? response.logs : []).slice(-Math.max(0, logLines)).map(snapshotLog)
44
+ }
45
+ function metadata(raw: CaptureDebugSnapshotRawResponse, input: SnapshotInput) {
46
+ const key = raw.ui_tree?.device ? `${raw.ui_tree.device.platform}:${raw.ui_tree.device.id}` : `${input.platform || 'unknown'}:${input.deviceId || 'default'}`
47
+ const value = deriveSnapshotMetadata(key, raw.ui_tree, 'snapshot', raw.ui_tree?.snapshot_revision ? null : raw.fingerprint || raw.activity || null)
48
+ raw.snapshot_revision = raw.ui_tree?.snapshot_revision ?? value.snapshot_revision
49
+ raw.captured_at_ms = raw.ui_tree?.captured_at_ms ?? value.captured_at_ms
50
+ raw.snapshot_delta = raw.ui_tree?.snapshot_delta ?? value.snapshot_delta ?? null
51
+ raw.loading_state = raw.ui_tree?.loading_state ?? value.loading_state
52
+ }
53
+ export async function collectDebugSnapshot(input: SnapshotInput = {}, deps: Dependencies) {
54
+ if (input.includeScreenshot === false) omitEvidence('screenshot')
55
+ if (input.includeLogs === false) omitEvidence('logs')
56
+ const timestamp = Date.now()
57
+ const raw: CaptureDebugSnapshotRawResponse = {
58
+ timestamp, snapshot_revision: 0, captured_at_ms: timestamp,
59
+ reason: input.reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: []
60
+ }
61
+ const entries = await collect(raw, input, deps)
62
+ if (input.includeLogs !== false) {
63
+ try { raw.logs = await collectLogs(entries, input, deps) }
64
+ catch (error) { raw.logs = []; raw.logs_error = error instanceof Error ? error.message : String(error) }
65
+ }
66
+ metadata(raw, input)
67
+ const semantic = deriveSnapshotSemantic(raw)
68
+ return semantic ? { raw, semantic } : { raw }
69
+ }
@@ -0,0 +1,26 @@
1
+ export interface SnapshotLogEntry { timestamp: number | null; level: string; message: string }
2
+ interface LogFields { message?: string; msg?: string; level?: string; levelName?: string; _level?: string; timestamp?: string | number; _iso?: string }
3
+ function levelOf(message: string, raw: string | undefined, stream: boolean) {
4
+ return (raw && String(raw).toUpperCase()) || (stream
5
+ ? /\bERROR\b/i.test(message) ? 'ERROR' : /\bWARN\b/i.test(message) ? 'WARN' : 'INFO'
6
+ : /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(message) ? 'ERROR' : /\b(WARN| W )\b/i.test(message) ? 'WARN' : 'INFO')
7
+ }
8
+ export function snapshotLog(item: unknown): SnapshotLogEntry {
9
+ if (!item) return { timestamp: null, level: 'INFO', message: '' }
10
+ if (typeof item === 'string') return { timestamp: null, level: levelOf(item, undefined, false), message: item }
11
+ const fields = item as LogFields
12
+ const message = fields.message || fields.msg || JSON.stringify(item)
13
+ const raw = fields.level || fields.levelName || fields._level || ''
14
+ const time = fields.timestamp || fields._iso || null
15
+ const timestamp = time && typeof time === 'string' ? (isNaN(new Date(time).getTime()) ? null : new Date(time).getTime()) : typeof time === 'number' ? time : null
16
+ return { timestamp, level: levelOf(message, raw, true), message }
17
+ }
18
+ export function streamLog(item: unknown): SnapshotLogEntry {
19
+ const fields = item as LogFields | null
20
+ const message = fields && (fields.message || fields.msg) ? fields.message || fields.msg! : typeof item === 'string' ? item : JSON.stringify(item)
21
+ const raw = fields && (fields.level || fields.levelName || fields._level) ? fields.level || fields.levelName || fields._level : ''
22
+ const iso = fields && (fields._iso || fields.timestamp)
23
+ let timestamp: number | null = null
24
+ if (iso && typeof iso === 'string') { const date = new Date(iso); if (!isNaN(date.getTime())) timestamp = date.getTime() }
25
+ return { timestamp, level: levelOf(message, raw, true), message }
26
+ }
@@ -38,7 +38,10 @@ function normalizeBounds(bounds: unknown): [number, number, number, number] | nu
38
38
  return normalized as [number, number, number, number]
39
39
  }
40
40
 
41
- function stableElementSignature(element: UIElement) {
41
+ type SignatureElement = Partial<Pick<UIElement, 'text' | 'contentDescription' | 'resourceId' | 'stable_id' | 'role' | 'test_tag' | 'selector' | 'clickable' | 'enabled' | 'visible' | 'state'>> & { bounds?: number[] | null; type?: string | null }
42
+ export interface SnapshotSignatureInput { elements?: SignatureElement[]; screen?: unknown; resolution?: unknown; error?: unknown }
43
+
44
+ function stableElementSignature(element: SignatureElement) {
42
45
  return {
43
46
  text: normalize(element.text),
44
47
  contentDescription: normalize(element.contentDescription),
@@ -115,7 +118,7 @@ function summarizeSnapshotDelta(previous: SnapshotState | undefined, currentElem
115
118
  }
116
119
  }
117
120
 
118
- export function computeSnapshotSignature(tree: Pick<GetUITreeResponse, 'elements' | 'screen' | 'resolution' | 'error'> | null | undefined): string | null {
121
+ export function computeSnapshotSignature(tree: SnapshotSignatureInput | null | undefined): string | null {
119
122
  if (!tree || tree.error) return null
120
123
 
121
124
  const payload = {
@@ -162,34 +165,10 @@ export function deriveSnapshotMetadata(
162
165
  const existing = snapshotStateByDevice.get(deviceKey)
163
166
  const previous = existing && Date.now() - existing.updatedAt <= 10 * 60 * 1000 ? existing : undefined
164
167
  if (!previous && existing) snapshotStateByDevice.delete(deviceKey)
165
- const hasValidTree = !!tree && !tree.error
166
- const records = hasValidTree ? buildElementRecords(tree) : null
167
- const currentElementSignatures = records?.signatures ?? previous?.elementSignatures ?? new Map<string, string>()
168
- const currentElements = records?.elementsByIdentity ?? previous?.elements ?? new Map<string, UIElement>()
168
+ const { hasValidTree, currentElementSignatures, currentElements } = snapshotRecords(tree, previous)
169
169
 
170
170
  const revision = previous && (signature === null || previous.signature === signature) ? previous.revision : nextRevision++
171
- const history = previous?.history ?? new Map<number, Map<string, UIElement>>()
172
- const historyTimes = previous?.historyTimes ?? new Map<number, number>()
173
- const oversized = previous?.oversized ?? new Map<number, number>()
174
- if (hasValidTree) {
175
- history.set(revision, currentElements)
176
- historyTimes.set(revision, Date.now())
177
- if ((tree.elements?.length ?? 0) > 500) oversized.set(revision, (tree.elements?.length ?? 0) - 500)
178
- }
179
- for (const [id, timestamp] of historyTimes) {
180
- if (Date.now() - timestamp > 10 * 60 * 1000) {
181
- history.delete(id)
182
- historyTimes.delete(id)
183
- oversized.delete(id)
184
- }
185
- }
186
- while (history.size > 8) {
187
- const oldest = history.keys().next().value
188
- if (oldest === undefined) break
189
- history.delete(oldest)
190
- historyTimes.delete(oldest)
191
- oversized.delete(oldest)
192
- }
171
+ const { history, historyTimes, oversized } = retainHistory(previous, revision, currentElements, tree, hasValidTree)
193
172
 
194
173
  snapshotStateByDevice.set(deviceKey, {
195
174
  revision,
@@ -226,19 +205,20 @@ export function getStateDelta(deviceKey: string, baseRevision: number, currentRe
226
205
  }
227
206
  let index = 0
228
207
  for (const [identity, element] of base) {
229
- if (!current.has(identity)) removed.push({ stable_id: element.stable_id ?? undefined, resourceId: element.resourceId ?? undefined, contentDescription: element.contentDescription ?? undefined, text: element.text ?? undefined, index })
208
+ if (!current.has(identity)) removed.push(removedElement(element, index))
230
209
  index++
231
210
  }
232
211
  const all = added.length + changed.length + removed.length
233
212
  const limit = 200
213
+ const truncated = all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision)
234
214
  return {
235
215
  base_snapshot_revision: baseRevision,
236
216
  snapshot_revision: currentRevision,
237
217
  added: added.slice(0, limit),
238
218
  changed: changed.slice(0, Math.max(0, limit - added.length)),
239
219
  removed: removed.slice(0, Math.max(0, limit - added.length - changed.length)),
240
- truncated: all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision),
241
- ...((all > limit || state.oversized.has(baseRevision) || state.oversized.has(currentRevision))
220
+ truncated,
221
+ ...((truncated)
242
222
  ? { omitted_changes: Math.max(all - limit, state.oversized.get(baseRevision) ?? 0, state.oversized.get(currentRevision) ?? 0) } : {})
243
223
  }
244
224
  }
@@ -262,3 +242,43 @@ export function resetSnapshotMetadataForTests() {
262
242
  snapshotStateByDevice.clear()
263
243
  nextRevision = 1
264
244
  }
245
+
246
+ function retainHistory(previous: SnapshotState | undefined, revision: number, currentElements: Map<string, UIElement>, tree: Parameters<typeof deriveSnapshotMetadata>[1], hasValidTree: boolean) {
247
+ const history = previous?.history ?? new Map<number, Map<string, UIElement>>()
248
+ const historyTimes = previous?.historyTimes ?? new Map<number, number>()
249
+ const oversized = previous?.oversized ?? new Map<number, number>()
250
+ if (hasValidTree) {
251
+ history.set(revision, currentElements)
252
+ historyTimes.set(revision, Date.now())
253
+ if ((tree!.elements?.length ?? 0) > 500) oversized.set(revision, (tree!.elements?.length ?? 0) - 500)
254
+ }
255
+ for (const [id, timestamp] of historyTimes) {
256
+ if (Date.now() - timestamp > 10 * 60 * 1000) {
257
+ history.delete(id)
258
+ historyTimes.delete(id)
259
+ oversized.delete(id)
260
+ }
261
+ }
262
+ while (history.size > 8) {
263
+ const oldest = history.keys().next().value
264
+ if (oldest === undefined) break
265
+ history.delete(oldest)
266
+ historyTimes.delete(oldest)
267
+ oversized.delete(oldest)
268
+ }
269
+
270
+ return { history, historyTimes, oversized }
271
+ }
272
+
273
+ function snapshotRecords(tree: Parameters<typeof deriveSnapshotMetadata>[1], previous: SnapshotState | undefined) {
274
+ const hasValidTree = !!tree && !tree.error
275
+ const records = hasValidTree ? buildElementRecords(tree) : null
276
+ const currentElementSignatures = records?.signatures ?? previous?.elementSignatures ?? new Map<string, string>()
277
+ const currentElements = records?.elementsByIdentity ?? previous?.elements ?? new Map<string, UIElement>()
278
+
279
+ return { hasValidTree, currentElementSignatures, currentElements }
280
+ }
281
+
282
+ function removedElement(element: UIElement, index: number) {
283
+ return { stable_id: element.stable_id ?? undefined, resourceId: element.resourceId ?? undefined, contentDescription: element.contentDescription ?? undefined, text: element.text ?? undefined, index }
284
+ }
@@ -0,0 +1,153 @@
1
+ import type {
2
+ CaptureDebugSnapshotRawResponse,
3
+ SnapshotSemanticResponse
4
+ } from '../types.js'
5
+ export interface SnapshotTreeElementLike {
6
+ text?: string | null
7
+ contentDescription?: string | null
8
+ contentDesc?: string | null
9
+ accessibilityLabel?: string | null
10
+ resourceId?: string | null
11
+ id?: string | null
12
+ type?: string | null
13
+ class?: string | null
14
+ clickable?: boolean
15
+ enabled?: boolean
16
+ visible?: boolean
17
+ state?: unknown
18
+ stable_id?: string | null
19
+ role?: string | null
20
+ test_tag?: string | null
21
+ selector?: unknown
22
+ semantic?: unknown
23
+ }
24
+
25
+ export interface SnapshotTreeLike {
26
+ screen?: string | null
27
+ elements?: SnapshotTreeElementLike[]
28
+ }
29
+
30
+ export function normalizeHint(value: unknown): string {
31
+ if (value === null || value === undefined) return ''
32
+ return String(value).trim().replace(/\s+/g, ' ').toLowerCase()
33
+ }
34
+
35
+ export function titleCase(value: string): string {
36
+ return value
37
+ .replace(/[_-]+/g, ' ')
38
+ .replace(/\s+/g, ' ')
39
+ .trim()
40
+ .replace(/\b\w/g, (match) => match.toUpperCase())
41
+ }
42
+
43
+ export function shortActivityName(activity: string | null | undefined): string | null {
44
+ if (!activity) return null
45
+ const trimmed = String(activity).trim()
46
+ if (!trimmed) return null
47
+ const lastSegment = trimmed.split('.').pop() || trimmed
48
+ const withoutSuffix = lastSegment.replace(/Activity$/, '')
49
+ return withoutSuffix ? titleCase(withoutSuffix) : titleCase(lastSegment)
50
+ }
51
+
52
+ function elementText(element: SnapshotTreeElementLike | undefined) {
53
+ return normalizeHint(element?.text ?? element?.contentDescription ?? element?.contentDesc ?? element?.accessibilityLabel ?? element?.resourceId ?? element?.id ?? '')
54
+ }
55
+ export function collectSnapshotTexts(tree: SnapshotTreeLike | null | undefined) {
56
+ const elements = Array.isArray(tree?.elements) ? tree!.elements! : []
57
+ const texts: string[] = []
58
+ const actionables: string[] = []
59
+
60
+ for (const element of elements) {
61
+ const text = elementText(element)
62
+ if (text) texts.push(text)
63
+ if (element?.clickable && element?.enabled !== false && text) {
64
+ actionables.push(text)
65
+ }
66
+ }
67
+
68
+ return {
69
+ texts: Array.from(new Set(texts)),
70
+ actionables: Array.from(new Set(actionables))
71
+ }
72
+ }
73
+
74
+ export function inferSnapshotScreen(raw: CaptureDebugSnapshotRawResponse): string | null {
75
+ const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
76
+ const treeScreen = normalizeHint(tree?.screen)
77
+ if (treeScreen) return titleCase(treeScreen)
78
+
79
+ const activity = shortActivityName(raw.activity)
80
+ if (activity) return activity
81
+
82
+ const { texts } = collectSnapshotTexts(tree)
83
+ if (texts.length > 0) return titleCase(texts[0])
84
+
85
+ return null
86
+ }
87
+
88
+ export function deriveSnapshotSemantic(raw: CaptureDebugSnapshotRawResponse): SnapshotSemanticResponse | null {
89
+ const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
90
+ const { texts, actionables } = collectSnapshotTexts(tree)
91
+ const screenFromTree = normalizeHint(tree?.screen)
92
+ const activityHint = normalizeHint(raw.activity)
93
+ const screen = inferSnapshotScreen(raw)
94
+
95
+ if (!screen && !activityHint && texts.length === 0 && !raw.logs.length) return null
96
+
97
+ const hasErrorLogs = raw.logs.some((entry) => /error|fatal exception|exception|failed/i.test(entry.message))
98
+ const hasLoadingSignals = texts.some((text) => /loading|please wait|spinner|progress/i.test(text))
99
+ const hasPrimaryText = texts.some((text) => /sign in|log in|login|home|checkout|settings|menu|profile|search/i.test(text))
100
+ const hasScreenshot = typeof raw.screenshot === 'string' && raw.screenshot.length > 0
101
+ const hasUiTree = !!tree && Array.isArray(tree.elements)
102
+
103
+ const signals: Record<string, string | number | boolean> = {
104
+ has_activity: !!activityHint,
105
+ has_ui_tree: hasUiTree,
106
+ has_screenshot: hasScreenshot,
107
+ has_visible_text: texts.length > 0,
108
+ has_clickable_elements: actionables.length > 0,
109
+ has_error_logs: hasErrorLogs,
110
+ has_loading_signals: hasLoadingSignals,
111
+ has_primary_text: hasPrimaryText
112
+ }
113
+
114
+ const warnings = snapshotWarnings(screenFromTree, activityHint, hasUiTree, hasErrorLogs)
115
+ const confidence = snapshotConfidence(hasUiTree, screen, activityHint, actionables.length, texts.length, hasScreenshot, hasErrorLogs, hasLoadingSignals)
116
+
117
+ if (!screen && confidence < 0.3) return null
118
+
119
+ return {
120
+ screen,
121
+ signals,
122
+ actions_available: actionables.length > 0 ? actionables.slice(0, 10) : null,
123
+ confidence,
124
+ warnings
125
+ }
126
+ }
127
+
128
+ function snapshotWarnings(screenFromTree: string, activityHint: string, hasUiTree: boolean, hasErrorLogs: boolean) {
129
+ const warnings: string[] = []
130
+ if (screenFromTree && activityHint && screenFromTree !== activityHint) {
131
+ warnings.push('ui_tree.screen and activity hints differ')
132
+ }
133
+ if (!hasUiTree) warnings.push('ui tree unavailable')
134
+ if (!activityHint) warnings.push('activity unavailable')
135
+ if (hasErrorLogs) warnings.push('error signals present in logs')
136
+
137
+ return warnings
138
+ }
139
+
140
+ function snapshotConfidence(hasUiTree: boolean, screen: string | null, activityHint: string, actionableCount: number, textCount: number, hasScreenshot: boolean, hasErrorLogs: boolean, hasLoadingSignals: boolean) {
141
+ const evidenceScore =
142
+ (hasUiTree ? 0.35 : 0) +
143
+ (screen ? 0.2 : 0) +
144
+ (activityHint ? 0.15 : 0) +
145
+ (actionableCount > 0 ? 0.15 : 0) +
146
+ (textCount > 0 ? 0.1 : 0) +
147
+ (hasScreenshot ? 0.05 : 0) +
148
+ (hasErrorLogs ? -0.15 : 0) +
149
+ (hasLoadingSignals ? -0.05 : 0)
150
+
151
+ const confidence = Math.max(0, Math.min(1, Number(evidenceScore.toFixed(2))))
152
+ return confidence
153
+ }
@@ -0,0 +1,3 @@
1
+ export class TraceError extends Error {
2
+ constructor(public code: string) { super(code) }
3
+ }
@@ -0,0 +1,61 @@
1
+ import { TraceError } from './system-trace-error.js'
2
+ import type { SystemTraceSessions, TraceResult } from './system-trace.js'
3
+ import { IosTraceSessions, type IosTraceResult } from './ios-trace/sessions.js'
4
+ import { IosTraceNative } from './ios-trace/native.js'
5
+ import type { IosTemplate } from './ios-trace/capabilities.js'
6
+
7
+ interface Route { platform: 'android' | 'ios'; device: string; active: boolean; touched: number }
8
+ export class SystemTraceRouter {
9
+ private routes = new Map<string, Route>()
10
+ private pending = new Set<string>()
11
+ private closing = false
12
+ constructor(private android: SystemTraceSessions, private ios = new IosTraceSessions(), private native = new IosTraceNative()) {}
13
+ capabilities(device: string) { return this.android.capabilities(device) }
14
+ iosCapabilities(device: string, appId?: string, template?: IosTemplate) { return this.native.capabilities(device, appId, template) }
15
+ private backend(platform: string) { return platform === 'ios' ? this.ios : this.android }
16
+ private track(result: TraceResult | IosTraceResult) {
17
+ this.routes.set(result.trace_id, { platform: result.device.platform, device: result.device.id,
18
+ active: result.cleanup.status !== 'complete', touched: performance.now() })
19
+ const terminal = [...this.routes.entries()].filter(([, r]) => !r.active)
20
+ for (const [index, [id, r]] of terminal.entries()) if (performance.now() - r.touched > 600000 || index < terminal.length - 100) this.routes.delete(id)
21
+ return result
22
+ }
23
+ async start(device: string, duration?: number, ios?: { appId: string; template?: IosTemplate }) {
24
+ if (this.closing) throw new TraceError('TRACE_BUSY')
25
+ // Refresh automatically finalised reservations without trusting cached state.
26
+ await Promise.all([...this.routes.entries()].filter(([, r]) => r.active).map(async ([id, r]) => {
27
+ try { this.track(await this.backend(r.platform).control(id, 'status')) }
28
+ catch { /* uncertain cleanup continues to reserve capacity */ }
29
+ }))
30
+ const key = (ios ? 'ios:' : 'android:') + device
31
+ const active = [...this.routes.values()].filter(r => r.active)
32
+ if (this.closing || this.pending.has(key) || active.some(r => r.platform + ':' + r.device === key) || active.length + this.pending.size >= 4) throw new TraceError('TRACE_BUSY')
33
+ this.pending.add(key)
34
+ try {
35
+ if (!ios) return this.track(await this.android.start(device, duration))
36
+ const cap = await this.native.capabilities(device, ios.appId, ios.template)
37
+ if (!cap.supported || !cap.device) throw new TraceError('TRACE_UNSUPPORTED')
38
+ if (this.closing) throw new TraceError('TRACE_BUSY')
39
+ return this.track(await this.ios.start(cap.device, ios.appId, duration))
40
+ } finally { this.pending.delete(key) }
41
+ }
42
+ async control(id: string, operation: 'status' | 'stop' | 'cancel' | 'duration') {
43
+ const route = this.routes.get(id)
44
+ if (!route) throw new TraceError('TRACE_NOT_FOUND')
45
+ return this.track(await this.backend(route.platform).control(id, operation))
46
+ }
47
+ async link(id: string, device: { platform: string; id: string }, correlation: string) {
48
+ const route = this.routes.get(id)
49
+ if (!route || route.platform !== device.platform || route.device !== device.id) throw new TraceError('TRACE_NOT_FOUND')
50
+ return this.backend(route.platform).link(id, device, correlation)
51
+ }
52
+ async shutdown() {
53
+ this.closing = true
54
+ this.native.shutdown()
55
+ const results = await Promise.allSettled([this.android.shutdown(), this.ios.shutdown()])
56
+ if (results.some(result => result.status === 'rejected')) throw new TraceError('TRACE_STOP_FAILED')
57
+ const remaining: (TraceResult | IosTraceResult)[] = []
58
+ for (const result of results) if (result.status === 'fulfilled') remaining.push(...result.value)
59
+ return remaining
60
+ }
61
+ }