mobile-debug-mcp 0.33.0 → 0.35.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 (448) 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/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +20 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,135 @@
1
+ import { DebuggerError, classify, diagnostic, atStage } from './diagnostics.js'
2
+ export { DebuggerError } from './diagnostics.js'
3
+ import { parseWorkerMessage, type WorkerEvent, type WorkerReply } from './protocol.js'
4
+ export type { WorkerEvent } from './protocol.js'
5
+ import { AsyncLocalStorage } from 'node:async_hooks'
6
+ import { execFile, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
7
+ import { promisify } from 'node:util'
8
+ import { readFile, mkdtemp, chmod, rm } from 'node:fs/promises'
9
+ import { createHash } from 'node:crypto'
10
+ import path from 'node:path'
11
+ import os from 'node:os'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { detectJavaHome } from '../utils/java.js'
14
+
15
+ const exec = promisify(execFile)
16
+ const operations = new AsyncLocalStorage<{ deadline: number; signal?: AbortSignal }>()
17
+ export function withDebuggerBudget<T>(timeout: number, signal: AbortSignal | undefined, operation: () => Promise<T>) {
18
+ return operations.run({ deadline: performance.now() + timeout, signal }, operation)
19
+ }
20
+ export function debuggerRemaining(fallback: number) {
21
+ const context = operations.getStore()
22
+ if (context?.signal?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
23
+ const remaining = Math.floor((context?.deadline ?? Infinity) - performance.now())
24
+ if (remaining <= 0) throw new DebuggerError('DEBUGGER_TIMEOUT')
25
+ return Math.min(fallback, remaining)
26
+ }
27
+ export function debuggerSignal() { return operations.getStore()?.signal }
28
+ export function nativeError(error: unknown, fallback: string): DebuggerError {
29
+ if (error instanceof DebuggerError) return error
30
+ if (debuggerSignal()?.aborted) return new DebuggerError('DEBUGGER_CANCELLED')
31
+ if (typeof error === 'object' && error !== null && 'killed' in error && error.killed) return new DebuggerError('DEBUGGER_TIMEOUT')
32
+ return new DebuggerError(fallback)
33
+ }
34
+ export const workerSource = fileURLToPath(new URL('../../src/debugger/java/DebuggerWorker.java', import.meta.url))
35
+ let compiled: Promise<{ java: string; directory: string }> | undefined
36
+ export async function prepareWorker() {
37
+ let stage: 'prerequisite' | 'compilation' = 'prerequisite'
38
+ compiled ??= (async () => {
39
+ const home = await detectJavaHome({ quiet: true }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
40
+ const java = home ? path.join(home, 'bin', 'java') : 'java'
41
+ const javac = home ? path.join(home, 'bin', 'javac') : 'javac'
42
+ const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', debuggerSignal()?.aborted ? 'cancelled' : typeof e === 'object' && e !== null && 'killed' in e && e.killed ? 'timeout' : 'unavailable') })
43
+ const major = Number(/version "(\d+)/.exec(version.stderr)?.[1])
44
+ if (!(major >= 17)) throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('prerequisite', 'unsupported'))
45
+ stage = 'compilation'
46
+ const source = await readFile(workerSource).catch(e => { throw classify(e, 'compilation', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
47
+ const key = createHash('sha256').update(source).update(version.stderr).digest('hex').slice(0, 16)
48
+ const directory = await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`)))
49
+ try { await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => chmod(directory, 0o700)); await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 }) }
50
+ catch (e) { await rm(directory, { recursive: true, force: true }); throw classify(nativeError(e, 'DEBUGGER_UNSUPPORTED'), 'compilation', 'DEBUGGER_UNSUPPORTED', 'operation_failed') }
51
+ return { java, directory }
52
+ })().catch(error => { compiled = undefined; throw classify(error, stage, 'DEBUGGER_ATTACH_FAILED') })
53
+ return compiled
54
+ }
55
+ function commandStage(command: string) { return command === 'attach' ? 'attach' as const : command === 'detach' ? 'cleanup' as const : 'command' as const }
56
+ export interface DebugWorker {
57
+ call(command: string, args?: Record<string, unknown>, timeout?: number): Promise<WorkerReply>
58
+ close(): Promise<void>
59
+ }
60
+ type WorkerFailureReason = 'spawn_failed' | 'process_exited' | 'write_failed' | 'invalid_message' | 'message_limit'
61
+ export class JdiWorker implements DebugWorker {
62
+ private child: ChildProcessWithoutNullStreams
63
+ private pending = new Map<number, { resolve(value: WorkerReply): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout>; command: string }>()
64
+ private sequence = 0
65
+ private dead = false
66
+ private failure?: DebuggerError
67
+ private replied = false
68
+ private exited: Promise<void>
69
+ constructor(java: string, directory: string, private onEvent: (event: WorkerEvent) => void) {
70
+ this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] })
71
+ let buffer = ''
72
+ this.exited = new Promise(resolve => { this.child.once('exit', () => { this.fail('process_exited'); resolve() }); this.child.once('error', () => { this.fail('spawn_failed'); resolve() }) })
73
+ this.child.stdin.on('error', () => this.fail('write_failed'))
74
+ this.child.stderr.resume() // Never copy worker data or VM diagnostics into MCP stdout/logs.
75
+ this.child.stdout.setEncoding('utf8')
76
+ this.child.stdout.on('data', (chunk: string) => {
77
+ buffer += chunk
78
+ if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { this.fail('message_limit'); this.child.kill('SIGKILL'); return }
79
+ let end: number
80
+ while ((end = buffer.indexOf('\n')) >= 0) {
81
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1)
82
+ try {
83
+ if (Buffer.byteLength(line) > 1024 * 1024) throw new Error('IPC_LIMIT')
84
+ const msg = parseWorkerMessage(JSON.parse(line))
85
+ if (msg.kind === 'event') onEvent(msg.value)
86
+ else {
87
+ this.replied = true
88
+ const request = this.pending.get(msg.id)
89
+ if (!request) continue
90
+ clearTimeout(request.timer); this.pending.delete(msg.id)
91
+ if (msg.error) request.reject(classify(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED', msg.error.diagnostic), commandStage(request.command), 'DEBUGGER_WORKER_FAILED'))
92
+ else request.resolve(msg.result)
93
+ }
94
+ } catch { this.fail(Buffer.byteLength(line) > 1024 * 1024 ? 'message_limit' : 'invalid_message'); this.child.kill('SIGKILL'); return }
95
+ }
96
+ })
97
+ }
98
+ private fail(reason: WorkerFailureReason) {
99
+ if (this.dead) return
100
+ this.dead = true
101
+ const stage = reason === 'spawn_failed' || (reason === 'process_exited' && !this.replied) ? 'worker_start' : 'ipc'
102
+ this.failure = new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic(stage, reason))
103
+ for (const request of this.pending.values()) {
104
+ clearTimeout(request.timer)
105
+ request.reject(this.failure)
106
+ }
107
+ this.pending.clear()
108
+ this.onEvent({ event: 'worker_failed', diagnostic: this.failure.diagnostic })
109
+ }
110
+ call(command: string, args: Record<string, unknown> = {}, timeout = 10000): Promise<WorkerReply> {
111
+ if (this.dead) return Promise.reject(this.failure ?? new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'unknown')))
112
+ try { timeout = Math.min(timeout, debuggerRemaining(timeout)) } catch (e) { return Promise.reject(classify(e, commandStage(command), 'DEBUGGER_WORKER_FAILED')) }
113
+ const id = ++this.sequence
114
+ const message = JSON.stringify({ ...args, command, id }) + '\n'
115
+ if (Buffer.byteLength(message) > 1024 * 1024) return Promise.reject(new DebuggerError('INVALID_ARGUMENT'))
116
+ return new Promise((resolve, reject) => {
117
+ const signal = debuggerSignal()
118
+ const abort = () => { this.pending.delete(id); clearTimeout(timer); done(); reject(new DebuggerError('DEBUGGER_CANCELLED', diagnostic(commandStage(command), 'cancelled'))) }
119
+ const done = () => signal?.removeEventListener('abort', abort)
120
+ const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT', diagnostic(commandStage(command), 'timeout'))) }, timeout)
121
+ this.pending.set(id, { command, resolve: value => { done(); resolve(value) }, reject: error => { done(); reject(error) }, timer })
122
+ signal?.addEventListener('abort', abort, { once: true })
123
+ try { this.child.stdin.write(message) } catch { this.fail('write_failed') }
124
+ })
125
+ }
126
+ async close() {
127
+ this.child.stdin.end()
128
+ const kill = setTimeout(() => this.child.kill('SIGKILL'), 2000)
129
+ try { await this.exited } finally { clearTimeout(kill) }
130
+ }
131
+ }
132
+ export async function createWorker(onEvent: (event: WorkerEvent) => void) {
133
+ const { java, directory } = await prepareWorker()
134
+ return new JdiWorker(java, directory, onEvent)
135
+ }
@@ -0,0 +1,83 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { ActionFailureCode, ActionTargetResolved, AdjustControlResponse, DeviceInfo, FindElementResponse, TraceStep } from '../../types.js'
3
+ import type { UiSelector } from '../../contracts/selector.js'
4
+ import type { UiElement, ResolvedUiElementContext } from '../model.js'
5
+ import type { InputService } from '../input-service.js'
6
+ import type { AdjustmentVerificationDependencies } from './adjustment-verification.js'
7
+ import { createTraceStep, buildActionExecutionResult } from '../../runtime/action-results.js'
8
+ import { completeAdjustment } from './adjustment-result.js'
9
+ import { planAdjustment } from './adjustment-plan.js'
10
+
11
+ export interface AdjustmentInput { selector?: UiSelector; element_id?: string; property?: string; targetValue: number; tolerance?: number; maxAttempts?: number; platform?: 'android' | 'ios'; deviceId?: string }
12
+ export interface ControlTree { elements?: UiElement[]; device?: DeviceInfo }
13
+ export type Match = { el: UiElement; idx: number }
14
+ export interface ControlMatch { tree: ControlTree | null; device?: DeviceInfo; match: Match | null; resolvedTarget: ActionTargetResolved | null }
15
+ export interface ControlWait { status?: string; element?: { elementId?: string | null } | null; device?: DeviceInfo; error?: { code?: string; message?: string } }
16
+ export interface AdjustmentDependencies {
17
+ clock?: Clock
18
+ observe(platform: 'android' | 'ios', deviceId?: string): Promise<ControlTree>
19
+ fingerprint(platform: 'android' | 'ios', deviceId?: string): Promise<string | null>
20
+ getHandle(id: string): ResolvedUiElementContext | undefined
21
+ currentHandle(elements: UiElement[], platform: 'android' | 'ios', deviceId: string | undefined, handle: ResolvedUiElementContext): { el: UiElement; index: number } | null
22
+ target(platform: 'android' | 'ios', deviceId: string | undefined, match: Match): ActionTargetResolved
23
+ wait(input: { selector: UiSelector; condition: 'clickable'; timeout_ms: number; poll_interval_ms: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<ControlWait | undefined>
24
+ find(input: { query: string; exact: boolean; timeoutMs: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<FindElementResponse>
25
+ expectState: AdjustmentVerificationDependencies['expectState']
26
+ service(platform: 'android' | 'ios', deviceId?: string): Promise<{ interact: Pick<InputService, 'setAdjustableValue' | 'adjustAccessibleValue'>; resolved: { id: string } }>
27
+ tap(input: { platform: 'android' | 'ios'; x: number; y: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo }>
28
+ swipe(input: { platform: 'android' | 'ios'; x1: number; y1: number; x2: number; y2: number; duration: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo; error?: string }>
29
+ }
30
+ export type ActualState = AdjustControlResponse['actual_state']
31
+ export type Mode = AdjustControlResponse['adjustment_mode']
32
+ export type Verification = { verification: { reason?: string } | null; observedState: ActualState; withinTolerance: boolean }
33
+ export interface AdjustmentState {
34
+ clock: Clock
35
+ input: AdjustmentInput & { property: string }
36
+ platform: 'android' | 'ios'; deviceId?: string; stored: ResolvedUiElementContext | null
37
+ selector: Record<string, unknown> | null; tolerance: number | null; limit: number; before: string | null
38
+ fallback: FindElementResponse['element'] | null; trace: TraceStep[]; sequence: number
39
+ observed: ActualState; mode: Mode; target: ActionTargetResolved | null; device?: DeviceInfo
40
+ attempts: number; cached: Match | null; coordinatePending: boolean
41
+ }
42
+ export type Iteration = ReturnType<typeof planAdjustment> & {
43
+ element: UiElement; index: number; actionDevice?: DeviceInfo; observed: ActualState; verification: Verification
44
+ }
45
+ export type StrategyOutcome = 'next' | 'retry' | AdjustControlResponse
46
+
47
+ export async function createAdjustment(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustmentState> {
48
+ const platform = input.platform || 'android'
49
+ const tolerance = typeof input.tolerance === 'number' && Number.isFinite(input.tolerance) ? Math.max(0, input.tolerance) : null
50
+ const limit = input.maxAttempts == null ? 5 : Math.max(1, Math.floor(Number(input.maxAttempts) || 1))
51
+ const stored = input.element_id ? deps.getHandle(input.element_id) ?? null : null
52
+ const before = await deps.fingerprint(platform, input.deviceId)
53
+ return { clock: deps.clock ?? systemClock, input: { ...input, property: input.property === undefined ? 'value' : input.property }, platform, deviceId: input.deviceId, stored,
54
+ selector: input.selector ?? (input.element_id ? { elementId: input.element_id } : null), tolerance, limit, before,
55
+ fallback: null, trace: [], sequence: 0, observed: null, mode: 'gesture', target: null,
56
+ attempts: 0, cached: null, coordinatePending: false }
57
+ }
58
+ export function recordAdjustment(s: AdjustmentState, stage: TraceStep['stage'], result: TraceStep['result'], metadata?: Record<string, unknown>) {
59
+ s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }))
60
+ }
61
+ export function failAdjustment(s: AdjustmentState, code: ActionFailureCode, reason: string, retryable = false,
62
+ target = s.target, device = s.device, observed = s.observed, attempts = s.attempts, mode: Mode = s.mode, after: string | null = null): AdjustControlResponse {
63
+ if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'failure', { reason, failure_code: code })
64
+ if (!s.trace.some(step => step.stage === 'recover')) recordAdjustment(s, 'recover', retryable ? 'retry' : 'failure', {
65
+ reason, failure_code: code, retry_allowed: retryable, recovery_attempts: attempts, retry_depth: attempts })
66
+ const tolerance = s.tolerance ?? 0
67
+ const base = buildActionExecutionResult({ actionType: 'adjust_control', sourceModule: 'interact', device,
68
+ selector: s.selector, resolved: target, success: false, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
69
+ failure: { failureCode: code, retryable }, traceSteps: s.trace,
70
+ details: { target_value: s.input.targetValue, tolerance, property: s.input.property, attempts,
71
+ adjustment_mode: mode, actual_state: observed, converged: false, within_tolerance: false, reason } })
72
+ return { ...base, target_state: { property: s.input.property, target_value: s.input.targetValue, tolerance },
73
+ actual_state: observed, within_tolerance: false, converged: false, attempts, adjustment_mode: mode }
74
+ }
75
+ export async function succeedAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies,
76
+ reason = 'control converged to target value', mode = s.mode, device = p.actionDevice ?? s.device) {
77
+ const after = await deps.fingerprint(s.platform, s.deviceId)
78
+ return completeAdjustment({ actionType: 'adjust_control', sourceModule: 'interact', device,
79
+ selector: s.selector, resolved: s.target, success: true, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
80
+ traceSteps: s.trace, details: { target_value: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
81
+ property: s.input.property, attempts: s.attempts, adjustment_mode: mode, actual_state: p.observed,
82
+ converged: true, within_tolerance: true, reason } })
83
+ }
@@ -0,0 +1,48 @@
1
+ import { _controlAxis, _buildConservativeControlPoint, _buildControlPoint, _buildAdjustmentProbePoints } from './control-geometry.js'
2
+ import { recordAdjustment, succeedAdjustment, failAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
3
+ import { verifyControl } from './adjustment-semantic.js'
4
+
5
+ function points(p: Iteration) {
6
+ const bounds = p.bounds!, range = p.valueRange!, value = p.currentValue!
7
+ const axis = _controlAxis(p.element, bounds)
8
+ return { axis, target: _buildConservativeControlPoint(bounds, p.normalizedTargetValue, value, range.min, range.max, axis),
9
+ current: _buildControlPoint(bounds, (value - range.min) / (range.max - range.min), axis) }
10
+ }
11
+ export async function coordinateAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
12
+ if (s.coordinatePending) return 'next'
13
+ const geometry = points(p)
14
+ const probes = p.inferredSliderState ? [geometry.target] : _buildAdjustmentProbePoints(p.bounds!, p.normalizedTargetValue, p.currentValue!, p.valueRange!.min, p.valueRange!.max, geometry.axis)
15
+ for (let i = 0; i < probes.length && s.attempts < s.limit; i++) {
16
+ const point = probes[i]
17
+ s.mode = 'coordinate'
18
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'coordinate', point })
19
+ const action = await deps.tap({ platform: s.platform, x: point.x, y: point.y, deviceId: s.deviceId })
20
+ s.attempts++; p.actionDevice = action.device ?? p.actionDevice
21
+ if (!action.success) {
22
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts, mode: 'coordinate', point, success: false }); continue
23
+ }
24
+ await verifyControl(s, p, deps, 'control did not converge yet')
25
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
26
+ s.coordinatePending = true; s.cached = null
27
+ if (s.attempts < s.limit) return 'retry'
28
+ }
29
+ return 'next'
30
+ }
31
+ export async function gestureAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
32
+ if (s.attempts >= s.limit) return 'next'
33
+ const geometry = points(p)
34
+ s.coordinatePending = false; s.mode = 'gesture'
35
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'gesture', start: geometry.current, end: geometry.target })
36
+ const action = await deps.swipe({ platform: s.platform, x1: geometry.current.x, y1: geometry.current.y,
37
+ x2: geometry.target.x, y2: geometry.target.y, duration: p.inferredSliderState ? 520 : 220, deviceId: s.deviceId })
38
+ s.attempts++
39
+ if (!action.success) {
40
+ recordAdjustment(s, 'execute', 'failure', { attempt: s.attempts, mode: 'gesture', start: geometry.current, end: geometry.target, success: false })
41
+ return failAdjustment(s, 'UNKNOWN', action.error ?? 'adjustment gesture failed', false, s.target,
42
+ action.device ?? p.actionDevice, p.observed ?? p.actualState)
43
+ }
44
+ p.actionDevice = action.device ?? p.actionDevice
45
+ await verifyControl(s, p, deps, 'gesture adjustment did not converge yet')
46
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
47
+ return 'next'
48
+ }
@@ -0,0 +1,23 @@
1
+ import type { UiElement } from '../model.js'
2
+ import type { AdjustControlResponse } from '../../types.js'
3
+ import { _normalizeBounds } from '../targeting/element-helpers.js'
4
+ import { _inferLabeledSliderState } from './slider-labels.js'
5
+ import { _readNumericControlRange, _readControlStep, _quantizeTargetValue,
6
+ _defaultAdjustTolerance, _readNumericControlValue } from './control-values.js'
7
+
8
+ export function planAdjustment(currentEl: UiElement, elements: UiElement[], targetValue: number,
9
+ property: string, requestedTolerance: number | null) {
10
+ const inferredSliderState = _inferLabeledSliderState(elements, currentEl)
11
+ const nativeBounds = _normalizeBounds(currentEl.bounds)
12
+ const bounds = inferredSliderState?.touch_bounds ?? nativeBounds
13
+ const valueRange = _readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null
14
+ const step = _readControlStep(currentEl)
15
+ const normalizedTargetValue = valueRange ? _quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step) : targetValue
16
+ const effectiveTolerance = requestedTolerance ?? _defaultAdjustTolerance(valueRange, step)
17
+ const currentValue = _readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null
18
+ const actualState: AdjustControlResponse['actual_state'] = currentValue !== null ? {
19
+ property, value: currentValue,
20
+ raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : inferredSliderState?.current
21
+ } : null
22
+ return { inferredSliderState, bounds, valueRange, normalizedTargetValue, effectiveTolerance, currentValue, actualState }
23
+ }
@@ -0,0 +1,104 @@
1
+ import type { UiSelector } from '../../contracts/selector.js'
2
+ import type { ControlWait } from './adjustment-context.js'
3
+ import type { FindElementResponse } from '../../types.js'
4
+ import type { UiElement } from '../model.js'
5
+ import { _normalizeBounds, _isAdjustableControl, _findFirstMatchingElement, _isAdjustableActionable } from '../targeting/element-helpers.js'
6
+ import { planAdjustment } from './adjustment-plan.js'
7
+ import { failAdjustment, recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type ControlMatch, type Iteration } from './adjustment-context.js'
8
+
9
+ function fallbackBounds(fallback: FindElementResponse['element']) {
10
+ const bounds = fallback?.bounds
11
+ if (Array.isArray(bounds)) return _normalizeBounds(bounds)
12
+ return bounds && typeof bounds === 'object' ? _normalizeBounds([Number(bounds.left), Number(bounds.top), Number(bounds.right), Number(bounds.bottom)]) : null
13
+ }
14
+ function findFallback(elements: UiElement[], fallback: FindElementResponse['element']) {
15
+ const bounds = fallbackBounds(fallback)
16
+ if (!bounds) return null
17
+ let index = elements.findIndex(el => {
18
+ const current = _normalizeBounds(el.bounds)
19
+ return !!current && current.every((value, i) => value === bounds[i])
20
+ })
21
+ if (index === -1) index = nearestControl(elements, bounds)
22
+ return index >= 0 && elements[index] ? { el: elements[index], idx: index } : null
23
+ }
24
+ function nearestControl(elements: UiElement[], bounds: [number, number, number, number]) {
25
+ const x = Math.floor((bounds[0] + bounds[2]) / 2), y = Math.floor((bounds[1] + bounds[3]) / 2)
26
+ let best = Infinity, index = -1
27
+ for (let i = 0; i < elements.length; i++) {
28
+ const el = elements[i]
29
+ if (!_isAdjustableControl(el)) continue
30
+ const current = _normalizeBounds(el.bounds)
31
+ if (!current) continue
32
+ const distance = Math.abs(Math.floor((current[0] + current[2]) / 2) - x) + Math.abs(Math.floor((current[1] + current[3]) / 2) - y)
33
+ if (distance < best) { best = distance; index = i }
34
+ }
35
+ return index
36
+ }
37
+ export async function resolveControl(s: AdjustmentState, deps: AdjustmentDependencies): Promise<ControlMatch | null> {
38
+ const tree = await deps.observe(s.platform, s.deviceId)
39
+ s.platform = tree?.device?.platform === 'ios' ? 'ios' : s.platform
40
+ s.deviceId = tree?.device?.id || s.deviceId
41
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
42
+ let match = null
43
+ if (s.input.element_id) {
44
+ const stored = deps.getHandle(s.input.element_id)
45
+ if (!stored) return null
46
+ const current = deps.currentHandle(elements, s.platform, s.deviceId, stored)
47
+ if (!current) return null
48
+ match = { el: current.el, idx: current.index }
49
+ } else {
50
+ if (s.fallback) match = findFallback(elements, s.fallback)
51
+ if (!match && s.input.selector) match = _findFirstMatchingElement(elements, s.input.selector)
52
+ }
53
+ return match ? { tree, device: tree?.device, match, resolvedTarget: deps.target(s.platform, s.deviceId, match) } : null
54
+ }
55
+ export async function initializeControl(s: AdjustmentState, deps: AdjustmentDependencies) {
56
+ const { selector, element_id } = s.input
57
+ if ((!selector && !element_id) || (selector && element_id)) {
58
+ return failAdjustment(s, 'ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', false, null, undefined, null, 0, 'gesture')
59
+ }
60
+ if (!selector || element_id) return null
61
+ const wait = await deps.wait({ selector, condition: 'clickable', timeout_ms: 5000, poll_interval_ms: 300, platform: s.platform, deviceId: s.deviceId })
62
+ if (wait?.status === 'success' && wait?.element?.elementId) {
63
+ s.input.element_id = wait.element.elementId; s.fallback = null; return null
64
+ }
65
+ return initializeFallback(s, deps, wait, selector)
66
+ }
67
+ async function initializeFallback(s: AdjustmentState, deps: AdjustmentDependencies, wait: ControlWait | undefined, selector: UiSelector) {
68
+ const query = selector.text ?? selector.resource_id ?? selector.accessibility_id ?? ''
69
+ if (!query) return failAdjustment(s, wait?.error?.code === 'ELEMENT_NOT_FOUND' ? 'ELEMENT_NOT_FOUND' : 'TIMEOUT',
70
+ wait?.error?.message ?? 'adjustable control not found', wait?.error?.code === 'ELEMENT_NOT_FOUND', null, wait?.device, null, 0, 'gesture')
71
+ const fallback = await deps.find({ query, exact: false, timeoutMs: 3000, platform: s.platform, deviceId: s.deviceId })
72
+ if (!fallback.found || !fallback.element) return failAdjustment(s, 'ELEMENT_NOT_FOUND', wait?.error?.message ?? 'adjustable control not found', true, null, wait?.device, null, 0, 'gesture')
73
+ s.fallback = fallback.element
74
+ return null
75
+ }
76
+ export async function prepareIteration(s: AdjustmentState, deps: AdjustmentDependencies) {
77
+ const resolved = s.cached ? { tree: null, device: s.device, match: s.cached, resolvedTarget: deps.target(s.platform, s.deviceId, s.cached) } : await resolveControl(s, deps)
78
+ if (!resolved?.match || !resolved.resolvedTarget) return failAdjustment(s, 'STALE_REFERENCE', 'adjustable control could not be resolved', true)
79
+ s.device = resolved.device; s.target = resolved.resolvedTarget; s.cached = resolved.match
80
+ const element = resolved.match.el
81
+ const actionable = _isAdjustableActionable(element, s.stored?.stable_id)
82
+ if (!actionable.actionable) return failAdjustment(s, actionable.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', actionable.reason ?? 'adjustable control is not actionable', true)
83
+ const elements = Array.isArray(resolved.tree?.elements) ? resolved.tree.elements : s.cached ? [s.cached.el] : [element]
84
+ const plan = planAdjustment(element, elements, s.input.targetValue, s.input.property, s.tolerance)
85
+ s.observed = plan.actualState
86
+ if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'success', { resolved_target: s.target, current_value: plan.currentValue, adjustment_mode: s.mode })
87
+ const iteration: Iteration = { ...plan, element, index: resolved.match.idx, actionDevice: s.device, observed: plan.actualState,
88
+ verification: { verification: null, observedState: plan.actualState, withinTolerance: false } }
89
+ const invalid = validateControl(s, iteration)
90
+ if (invalid) return invalid
91
+ if (plan.currentValue !== null && Math.abs(plan.currentValue - plan.normalizedTargetValue) <= plan.effectiveTolerance) {
92
+ recordAdjustment(s, 'verify', 'success', { property: s.input.property, target_value: plan.normalizedTargetValue, actual_state: plan.actualState, reason: 'control already within tolerance' })
93
+ return succeedAdjustment(s, iteration, deps, 'control already within tolerance', 'semantic', s.device)
94
+ }
95
+ if (!plan.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
96
+ return iteration
97
+ }
98
+ function validateControl(s: AdjustmentState, p: Iteration) {
99
+ if (s.input.property !== 'value' && s.input.property !== 'raw_value') return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only')
100
+ if (!_isAdjustableControl(p.element) && !p.inferredSliderState) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control')
101
+ if (p.currentValue === null) return failAdjustment(s, 'UNKNOWN', 'current numeric value is unavailable')
102
+ if (p.valueRange && (s.input.targetValue < p.valueRange.min || s.input.targetValue > p.valueRange.max)) return failAdjustment(s, 'CONTROL_CONVERGENCE_FAILED', `targetValue ${s.input.targetValue} is outside the control range ${p.valueRange.min}..${p.valueRange.max}`)
103
+ return null
104
+ }
@@ -0,0 +1,34 @@
1
+ import type { AdjustControlResponse } from '../../types.js'
2
+ import { buildActionExecutionResult } from '../../runtime/action-results.js'
3
+
4
+ type SuccessDetails = {
5
+ property: string
6
+ target_value: number
7
+ tolerance: number
8
+ attempts: number
9
+ adjustment_mode: AdjustControlResponse['adjustment_mode']
10
+ actual_state: AdjustControlResponse['actual_state']
11
+ reason: string
12
+ converged: true
13
+ within_tolerance: true
14
+ }
15
+ type AdjustmentSuccess = Parameters<typeof buildActionExecutionResult>[0] & { details: SuccessDetails }
16
+
17
+ /** Keep the action envelope and top-level control result in agreement. */
18
+ export function completeAdjustment(input: AdjustmentSuccess): AdjustControlResponse {
19
+ const base = buildActionExecutionResult(input)
20
+ const details = input.details
21
+ return {
22
+ ...base,
23
+ target_state: {
24
+ property: details.property,
25
+ target_value: details.target_value,
26
+ tolerance: details.tolerance
27
+ },
28
+ actual_state: details.actual_state,
29
+ within_tolerance: true,
30
+ converged: true,
31
+ attempts: details.attempts,
32
+ adjustment_mode: details.adjustment_mode
33
+ }
34
+ }
@@ -0,0 +1,53 @@
1
+ import { _hasDirectAdjustmentAction, _adjustmentDirections } from './control-values.js'
2
+ import { verifyAdjustment } from './adjustment-verification.js'
3
+ import { recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
4
+
5
+ export async function verifyControl(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies, fallback: string) {
6
+ p.verification = await verifyAdjustment({ labeled: !!p.inferredSliderState, currentEl: p.element, actualState: p.actualState,
7
+ property: s.input.property, target: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
8
+ expectation: { element_id: s.target?.elementId ?? s.input.element_id, selector: s.input.selector ?? undefined,
9
+ property: s.input.property, expected: p.normalizedTargetValue, platform: s.platform, deviceId: s.deviceId }
10
+ }, { observe: () => deps.observe(s.platform, s.deviceId), expectState: deps.expectState })
11
+ p.observed = p.verification.observedState; s.observed = p.observed
12
+ recordAdjustment(s, 'verify', p.verification.withinTolerance ? 'success' : 'retry', {
13
+ attempt: s.attempts, property: s.input.property, target_value: p.normalizedTargetValue,
14
+ actual_state: p.observed, reason: p.verification.verification?.reason ?? fallback })
15
+ }
16
+ export async function directAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
17
+ if (!_hasDirectAdjustmentAction(p.element) || s.attempts >= s.limit) return 'next'
18
+ const { interact, resolved } = await deps.service(s.platform, s.deviceId)
19
+ if (typeof interact.setAdjustableValue !== 'function') return 'next'
20
+ s.mode = 'semantic'
21
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'direct_set_value', target_value: p.normalizedTargetValue })
22
+ const action = await interact.setAdjustableValue({ deviceId: resolved.id, target: s.target, property: s.input.property, value: p.normalizedTargetValue })
23
+ s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
24
+ if (action?.success === false) return 'next'
25
+ await verifyControl(s, p, deps, 'direct adjustment did not converge yet')
26
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
27
+ s.cached = null
28
+ return s.attempts < s.limit ? 'retry' : 'next'
29
+ }
30
+ export async function incrementalAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
31
+ const directions = _adjustmentDirections(p.element)
32
+ if ((!directions.increment && !directions.decrement) || s.attempts >= s.limit) return 'next'
33
+ const { interact, resolved } = await deps.service(s.platform, s.deviceId)
34
+ const adjust = typeof interact.adjustAccessibleValue === 'function' ? interact.adjustAccessibleValue.bind(interact) : null
35
+ while (adjust && s.attempts < s.limit) {
36
+ const direction = p.normalizedTargetValue > (p.observed?.value ?? p.currentValue!) ? 'increment' : 'decrement'
37
+ if (!directionAllowed(direction, directions)) break
38
+ s.mode = 'semantic'
39
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'accessibility_increment_decrement', direction })
40
+ const action = await adjust({ deviceId: resolved.id, target: s.target, property: s.input.property, direction })
41
+ s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
42
+ if (action?.success === false) break
43
+ await verifyControl(s, p, deps, 'incremental adjustment did not converge yet')
44
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
45
+ s.cached = null
46
+ if (s.attempts < s.limit) return 'retry'
47
+ }
48
+ return 'next'
49
+ }
50
+
51
+ function directionAllowed(direction: 'increment' | 'decrement', directions: { increment: boolean; decrement: boolean }) {
52
+ return !((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement))
53
+ }
@@ -0,0 +1,60 @@
1
+ import type { AdjustControlResponse, ExpectStateResponse } from '../../types.js'
2
+ import type { UiElement } from '../model.js'
3
+ import type { UiSelector } from '../../contracts/selector.js'
4
+ import { _inferLabeledSliderState } from './slider-labels.js'
5
+ import { _parseNumericControlValue } from './control-values.js'
6
+
7
+ interface StateExpectation {
8
+ element_id?: string
9
+ selector?: UiSelector
10
+ property: string
11
+ expected: number
12
+ platform: 'android' | 'ios'
13
+ deviceId?: string
14
+ }
15
+ export interface AdjustmentVerificationInput {
16
+ labeled: boolean
17
+ currentEl: UiElement
18
+ actualState: AdjustControlResponse['actual_state']
19
+ property: string
20
+ target: number
21
+ tolerance: number
22
+ expectation: StateExpectation
23
+ }
24
+ export interface AdjustmentVerificationDependencies {
25
+ observe(): Promise<{ elements?: UiElement[] } | null>
26
+ expectState(input: StateExpectation): Promise<ExpectStateResponse>
27
+ }
28
+
29
+ /** Every verification obtains fresh evidence through its supplied observation boundary. */
30
+ export async function verifyAdjustment(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
31
+ if (input.labeled) return verifyLabeledControl(input, dependencies)
32
+ const verification = await dependencies.expectState(input.expectation)
33
+ const observedValue = _parseNumericControlValue(verification?.observed_state?.value) ??
34
+ _parseNumericControlValue(verification?.observed_state?.raw_value)
35
+ const observedState = observedValue !== null ? {
36
+ property: input.property, value: observedValue,
37
+ raw_value: typeof verification?.observed_state?.raw_value === 'number' ? verification.observed_state.raw_value : undefined
38
+ } : input.actualState
39
+ return { verification, observedState, withinTolerance: withinTolerance(observedValue, input) }
40
+ }
41
+ async function verifyLabeledControl(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
42
+ const tree = await dependencies.observe()
43
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
44
+ const refreshed = _inferLabeledSliderState(elements, input.currentEl)
45
+ const observedValue = refreshed?.current ?? null
46
+ const observedState = observedValue !== null ? {
47
+ property: input.property, value: observedValue, raw_value: observedValue
48
+ } : input.actualState
49
+ const matched = withinTolerance(observedValue, input)
50
+ return {
51
+ verification: {
52
+ success: matched, observed_state: observedState,
53
+ reason: observedValue !== null ? 'visible labeled value read from nearby slider labels' : 'visible labeled value unavailable'
54
+ },
55
+ observedState, withinTolerance: matched
56
+ }
57
+ }
58
+ function withinTolerance(value: number | null, input: AdjustmentVerificationInput) {
59
+ return value !== null && Math.abs(value - input.target) <= input.tolerance
60
+ }
@@ -0,0 +1,49 @@
1
+ import type { AdjustControlResponse } from '../../types.js'
2
+ import { createAdjustment, failAdjustment, recordAdjustment, succeedAdjustment,
3
+ type AdjustmentInput, type AdjustmentDependencies, type AdjustmentState, type Iteration } from './adjustment-context.js'
4
+ import { initializeControl, prepareIteration } from './adjustment-resolution.js'
5
+ import { directAdjustment, incrementalAdjustment } from './adjustment-semantic.js'
6
+ import { coordinateAdjustment, gestureAdjustment } from './adjustment-gestures.js'
7
+
8
+ function cacheObservation(s: AdjustmentState, p: Iteration) {
9
+ s.cached = p.inferredSliderState ? null : {
10
+ el: { ...p.element, state: { ...(p.element.state ?? null), ...(p.observed ? {
11
+ [p.observed.property]: p.observed.value, raw_value: p.observed.raw_value ?? p.observed.value
12
+ } : {}) } }, idx: p.index
13
+ }
14
+ }
15
+ async function attempt(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies) {
16
+ for (const strategy of [directAdjustment, incrementalAdjustment]) {
17
+ const outcome = await strategy(s, p, deps)
18
+ if (outcome !== 'next') return outcome
19
+ }
20
+ if (!p.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
21
+ if (!p.valueRange) return failAdjustment(s, 'UNKNOWN', 'value_range unavailable')
22
+ for (const strategy of [coordinateAdjustment, gestureAdjustment]) {
23
+ const outcome = await strategy(s, p, deps)
24
+ if (outcome !== 'next') return outcome
25
+ }
26
+ s.observed = p.observed
27
+ if (p.verification.withinTolerance) {
28
+ const reason = p.verification.verification?.reason ?? 'control converged to target value'
29
+ recordAdjustment(s, 'verify', 'success', { attempt: s.attempts, property: s.input.property,
30
+ target_value: p.normalizedTargetValue, actual_state: p.observed, reason })
31
+ return succeedAdjustment(s, p, deps, reason)
32
+ }
33
+ cacheObservation(s, p)
34
+ return 'next'
35
+ }
36
+ export async function adjustControl(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustControlResponse> {
37
+ const state = await createAdjustment(input, deps)
38
+ const invalid = await initializeControl(state, deps)
39
+ if (invalid) return invalid
40
+ for (let index = 0; index < state.limit && state.attempts < state.limit; index++) {
41
+ const prepared = await prepareIteration(state, deps)
42
+ if ('success' in prepared) return prepared
43
+ const outcome = await attempt(state, prepared, deps)
44
+ if (outcome !== 'retry' && outcome !== 'next') return outcome
45
+ }
46
+ const after = await deps.fingerprint(state.platform, state.deviceId)
47
+ return failAdjustment(state, 'CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', true,
48
+ state.target, state.device, state.observed, state.attempts, state.mode, after)
49
+ }