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,166 @@
1
+ import com.sun.jdi.*;
2
+ import com.sun.jdi.connect.*;
3
+ import com.sun.jdi.event.*;
4
+ import com.sun.jdi.request.*;
5
+ import java.io.*;
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.util.*;
8
+ import java.util.concurrent.*;
9
+
10
+ /** Standalone JDI bridge. No debuggee method invocation, evaluation or mutation. */
11
+ public final class DebuggerWorker {
12
+ static final int MAX_LINE = 1048576;
13
+ final Object lock = new Object();
14
+ VirtualMachine vm;
15
+ final Map<String, Map<String,Object>> breaks = new LinkedHashMap<>();
16
+ final Map<String, StackFrame> frames = new HashMap<>();
17
+ final Map<String, ThreadReference> threads = new HashMap<>();
18
+ final List<EventSet> held = new ArrayList<>();
19
+ String stopId;
20
+ long attachedAt, pausedAt, activityAt;
21
+ boolean closed;
22
+ StepRequest step;
23
+ final ScheduledExecutorService watchdog = Executors.newSingleThreadScheduledExecutor(r -> { Thread t=new Thread(r,"debugger-watchdog"); t.setDaemon(true); return t; });
24
+ static Map<String,Object> obj(Object... pairs) { Map<String,Object> m=new LinkedHashMap<>(); for(int i=0;i<pairs.length;i+=2)m.put((String)pairs[i],pairs[i+1]);return m; }
25
+ static synchronized void send(Object o) { System.out.println(Json.write(o)); System.out.flush(); }
26
+ static String str(Map<String,Object> m,String k) { Object v=m.get(k); return v==null?"":v.toString(); }
27
+ static int num(Map<String,Object> m,String k,int d) { Object v=m.get(k);return v instanceof Number?((Number)v).intValue():d; }
28
+ static RuntimeException fail(String code) { return new IllegalStateException(code); }
29
+ void active() { if(vm==null || closed)throw fail("DEBUGGER_DISCONNECTED"); }
30
+ void paused(Map<String,Object> a) { active();if(stopId==null)throw fail("DEBUGGER_NOT_PAUSED");if(!stopId.equals(str(a,"stopId")))throw fail("DEBUGGER_STALE_REFERENCE"); }
31
+ void invalidate() { stopId=null;frames.clear();threads.clear();pausedAt=0; }
32
+ void cancelStep() { if(step!=null){try{vm.eventRequestManager().deleteEventRequest(step);}catch(RuntimeException ignored){}step=null;} }
33
+ void release() { for(EventSet s:held)s.resume();held.clear();invalidate(); }
34
+ Map<String,Object> location(Location l) { String source=null;try{source=l.sourceName();}catch(AbsentInformationException ignored){}return obj("class_name",l.declaringType().name(),"method",l.method().name(),"source",source,"line",l.lineNumber(),"code_index",l.codeIndex(),"class_loader_id",l.declaringType().classLoader()==null?"bootstrap":String.valueOf(l.declaringType().classLoader().uniqueID())); }
35
+ String thread(ThreadReference t) { String id=stopId+":"+t.uniqueID();threads.put(id,t);return id; }
36
+ Map<String,Object> state() {return obj("state",closed?"detached":stopId==null?"running":"paused","stop_id",stopId,"breakpoints",new ArrayList<>(breaks.values()));}
37
+ void bind(Map<String,Object> b,ReferenceType type) {
38
+ try {
39
+ List<Location> ls=type.locationsOfLine(((Number)b.get("line")).intValue());
40
+ if(ls.isEmpty()){ if(!"verified".equals(b.get("status")))b.put("status","rejected"); b.put("reason","DEBUGGER_LOCATION_UNAVAILABLE");return; }
41
+ @SuppressWarnings("unchecked") List<Object> locations=(List<Object>)b.get("locations");
42
+ for(Location l:ls){
43
+ Map<String,Object> loc=location(l); if(locations.contains(loc))continue;
44
+ BreakpointRequest req=vm.eventRequestManager().createBreakpointRequest(l);req.putProperty("id",b.get("breakpoint_id"));req.setSuspendPolicy(EventRequest.SUSPEND_ALL);req.enable();locations.add(loc);
45
+ }
46
+ b.put("status","verified");b.remove("reason");
47
+ } catch(AbsentInformationException e){if(!"verified".equals(b.get("status")))b.put("status","rejected");b.put("reason","DEBUGGER_LOCATION_UNAVAILABLE");}
48
+ }
49
+ void eventLoop() {
50
+ try {
51
+ while(!closed){
52
+ EventSet set=vm.eventQueue().remove(200);
53
+ if(set==null)continue;
54
+ synchronized(lock){
55
+ if(closed)break;
56
+ List<Object> stops=new ArrayList<>();ThreadReference trigger=null;Set<ReferenceType> prepared=new HashSet<>();
57
+ for(Event event:set){
58
+ if(event instanceof VMDeathEvent || event instanceof VMDisconnectEvent){finish("process_exit");return;}
59
+ if(event instanceof ClassPrepareEvent c && prepared.add(c.referenceType())){ for(Map<String,Object>b:breaks.values())if(!"removed".equals(b.get("status")) && b.get("className").equals(c.referenceType().name())){bind(b,c.referenceType());send(obj("event","breakpoint","breakpoint",b));} }
60
+ if(event instanceof BreakpointEvent b){trigger=b.thread();stops.add(obj("reason","breakpoint","location",location(b.location()),"breakpoint_id",b.request().getProperty("id")));}
61
+ if(event instanceof StepEvent s){trigger=s.thread();stops.add(obj("reason","step","location",location(s.location())));}
62
+ }
63
+ if(!stops.isEmpty()){
64
+ cancelStep();held.add(set);
65
+ // Additional simultaneously suspended sets belong to the same epoch.
66
+ if(stopId==null){stopId=UUID.randomUUID().toString();pausedAt=System.nanoTime();}
67
+ send(obj("event","stopped","state","paused","stop_id",stopId,"thread_id",thread(trigger),"stops",stops));
68
+ }else set.resume();
69
+ }
70
+ }
71
+ }catch(InterruptedException e){Thread.currentThread().interrupt();}catch(RuntimeException e){synchronized(lock){finish("transport_lost");}}
72
+ }
73
+ void finish(String reason) {
74
+ if(closed)return;
75
+ boolean clean=true;
76
+ try{if(vm!=null){cancelStep();vm.dispose();}}catch(VMDisconnectedException ignored){}catch(RuntimeException e){clean=false;}
77
+ closed=true;held.clear();invalidate();
78
+ send(obj("event","terminated","reason",reason,"cleanup_complete",clean,"execution_state","unknown"));
79
+ }
80
+ Object command(Map<String,Object> a) throws Exception {
81
+ String cmd=str(a,"command");activityAt=System.nanoTime();
82
+ if(cmd.equals("attach")){
83
+ if(vm!=null)throw fail("DEBUGGER_BUSY");
84
+ AttachingConnector c=Bootstrap.virtualMachineManager().attachingConnectors().stream().filter(x->x.name().equals("com.sun.jdi.SocketAttach")).findFirst().orElseThrow();
85
+ Map<String,Connector.Argument> args=c.defaultArguments();args.get("hostname").setValue("127.0.0.1");args.get("port").setValue(str(a,"port"));args.get("timeout").setValue(str(a,"timeoutMs"));
86
+ vm=c.attach(args);attachedAt=System.nanoTime();
87
+ Thread t=new Thread(this::eventLoop,"debugger-events");t.setDaemon(true);t.start();
88
+ return state();
89
+ }
90
+ if(cmd.equals("detach")){finish("requested");return state();}
91
+ active();
92
+ if(cmd.equals("heartbeat"))return obj("alive",true);
93
+ if(cmd.equals("state"))return state();
94
+ if(cmd.equals("set")){
95
+ if(breaks.size()>=100)throw fail("DEBUGGER_BUSY");
96
+ String id=UUID.randomUUID().toString();Map<String,Object>b=obj("breakpoint_id",id,"className",str(a,"className"),"line",num(a,"line",0),"status","pending","locations",new ArrayList<>());
97
+ breaks.put(id,b);
98
+ ClassPrepareRequest req=vm.eventRequestManager().createClassPrepareRequest();req.addClassFilter(str(a,"className"));req.putProperty("id",id);req.setSuspendPolicy(EventRequest.SUSPEND_ALL);req.enable();
99
+ for(ReferenceType type:vm.classesByName(str(a,"className")))bind(b,type);
100
+ return b;
101
+ }
102
+ if(cmd.equals("remove")){
103
+ String id=str(a,"breakpointId");
104
+ if(!breaks.containsKey(id))throw fail("DEBUGGER_NOT_FOUND");
105
+ for(EventRequest r:new ArrayList<EventRequest>(vm.eventRequestManager().breakpointRequests()))if(id.equals(r.getProperty("id")))vm.eventRequestManager().deleteEventRequest(r);
106
+ for(EventRequest r:new ArrayList<EventRequest>(vm.eventRequestManager().classPrepareRequests()))if(id.equals(r.getProperty("id")))vm.eventRequestManager().deleteEventRequest(r);
107
+ breaks.get(id).put("status","removed");return obj("removed",true);
108
+ }
109
+ paused(a);
110
+ if(cmd.equals("stack")){
111
+ ThreadReference t=threads.get(str(a,"threadId"));if(t==null)throw fail("DEBUGGER_STALE_REFERENCE");
112
+ int count=Math.min(t.frameCount(),1000),offset=num(a,"offset",0),end=Math.min(count,offset+num(a,"limit",20));List<Object> out=new ArrayList<>();
113
+ for(int i=offset;i<end;i++){StackFrame f=t.frame(i);String id=stopId+":"+t.uniqueID()+":"+i;if(!frames.containsKey(id)&&frames.size()>=1000)throw fail("DEBUGGER_BUSY");frames.put(id,f);out.add(obj("frame_id",id,"index",i,"location",location(f.location())));}
114
+ return obj("frames",out,"next_offset",end<count?end:null,"truncated",t.frameCount()>1000);
115
+ }
116
+ if(cmd.equals("locals")){
117
+ StackFrame f=frames.get(str(a,"frameId"));if(f==null)throw fail("DEBUGGER_STALE_REFERENCE");List<Object> out=new ArrayList<>();
118
+ try{List<LocalVariable> vars=f.visibleVariables();if(vars.isEmpty())return obj("availability","unknown","reason","EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA","locals",out,"truncated",false);for(LocalVariable v:vars.subList(0,Math.min(vars.size(),100))){Value value=f.getValue(v);Object display=null;boolean truncated=false;
119
+ if(value instanceof StringReference s){String text=s.value();truncated=text.length()>256;display=text.substring(0,Math.min(text.length(),256));}
120
+ else if(value instanceof PrimitiveValue)display=value.toString();
121
+ else if(value instanceof ObjectReference o)display=obj("object_id",String.valueOf(o.uniqueID()),"type",o.referenceType().name());
122
+ out.add(obj("name",v.name(),"type",v.typeName(),"value",display,"truncated",truncated));}
123
+ return obj("availability","available","locals",out,"truncated",vars.size()>100);
124
+ }catch(AbsentInformationException e){return obj("availability","debug_metadata_unavailable","locals",out,"truncated",false);}
125
+ }
126
+ if(cmd.equals("resume") || cmd.equals("step")){
127
+ if(cmd.equals("step")){
128
+ ThreadReference t=threads.get(str(a,"threadId"));if(t==null)throw fail("DEBUGGER_STALE_REFERENCE");cancelStep();
129
+ int depth=switch(str(a,"kind")){case "into"->StepRequest.STEP_INTO;case "over"->StepRequest.STEP_OVER;case "out"->StepRequest.STEP_OUT;default->throw fail("INVALID_ARGUMENT");};
130
+ step=vm.eventRequestManager().createStepRequest(t,StepRequest.STEP_LINE,depth);step.addCountFilter(1);step.setSuspendPolicy(EventRequest.SUSPEND_ALL);step.enable();
131
+ }else cancelStep();
132
+ release();return state();
133
+ }
134
+ throw fail("INVALID_ARGUMENT");
135
+ }
136
+ void run() throws IOException {
137
+ watchdog.scheduleAtFixedRate(()->{synchronized(lock){if(vm==null||closed)return;long now=System.nanoTime();String reason=pausedAt!=0&&now-pausedAt>=TimeUnit.SECONDS.toNanos(60)?"pause_limit":now-activityAt>=TimeUnit.MINUTES.toNanos(5)?"idle_limit":now-attachedAt>=TimeUnit.MINUTES.toNanos(30)?"session_limit":null;if(reason!=null)finish(reason);}},1,1,TimeUnit.SECONDS);
138
+ Runtime.getRuntime().addShutdownHook(new Thread(()->{synchronized(lock){finish("worker_shutdown");}}));
139
+ try(Reader reader=new InputStreamReader(System.in,StandardCharsets.UTF_8)){
140
+ StringBuilder line=new StringBuilder();int ch;
141
+ while((ch=reader.read())!=-1){if(ch!='\n'){line.append((char)ch);if(line.length()>MAX_LINE)throw new IOException("IPC_LIMIT");continue;}
142
+ Object id=null;String commandName="";
143
+ try{@SuppressWarnings("unchecked") Map<String,Object>a=(Map<String,Object>)new Json(line.toString()).value();id=a.get("id");commandName=str(a,"command");synchronized(lock){Object result=command(a);send(obj("id",id,"result",result));}}
144
+ catch(Exception e){
145
+ String msg=e.getMessage();
146
+ String code=msg!=null&&msg.matches("(?:DEBUGGER|INVALID|PROCESS|DEVICE)_[A-Z_]+")?msg:e instanceof VMDisconnectedException?"DEBUGGER_DISCONNECTED":"DEBUGGER_WORKER_FAILED";
147
+ String stage=commandName.equals("attach")?"attach":e instanceof VMDisconnectedException?"transport":"command";
148
+ String reason=e instanceof java.net.ConnectException?"connection_refused":e instanceof VMDisconnectedException?"transport_lost":e instanceof java.net.SocketTimeoutException || e instanceof TransportTimeoutException?"timeout":"operation_failed";
149
+ send(obj("id",id,"error",obj("code",code,"diagnostic",obj("stage",stage,"reason",reason))));
150
+ }
151
+ line.setLength(0);
152
+ }
153
+ }finally{synchronized(lock){finish("parent_closed");}watchdog.shutdownNow();}
154
+ }
155
+ public static void main(String[] args) throws Exception {new DebuggerWorker().run();}
156
+
157
+ // Small strict JSON codec for the bounded, whitelisted bridge protocol.
158
+ static final class Json {
159
+ final String s;int p;Json(String s){this.s=s;}
160
+ void ws(){while(p<s.length()&&Character.isWhitespace(s.charAt(p)))p++;}
161
+ Object value(){Object v=read();ws();if(p!=s.length())throw fail("INVALID_ARGUMENT");return v;}
162
+ Object read(){ws();char c=s.charAt(p++);if(c=='"')return string();if(c=='{'){Map<String,Object>m=new LinkedHashMap<>();ws();if(s.charAt(p)=='}'){p++;return m;}do{ws();if(s.charAt(p++)!='"')throw fail("INVALID_ARGUMENT");String k=string();ws();if(s.charAt(p++)!=':')throw fail("INVALID_ARGUMENT");m.put(k,read());ws();c=s.charAt(p++);}while(c==',');if(c!='}')throw fail("INVALID_ARGUMENT");return m;}if(c=='['){List<Object>l=new ArrayList<>();ws();if(s.charAt(p)==']'){p++;return l;}do{l.add(read());ws();c=s.charAt(p++);}while(c==',');if(c!=']')throw fail("INVALID_ARGUMENT");return l;}p--;for(String lit:List.of("true","false","null"))if(s.startsWith(lit,p)){p+=lit.length();return lit.equals("null")?null:Boolean.valueOf(lit);}int start=p;while(p<s.length()&&"-+0123456789.eE".indexOf(s.charAt(p))>=0)p++;String number=s.substring(start,p);if(number.indexOf('.')<0&&number.indexOf('e')<0&&number.indexOf('E')<0)return Long.valueOf(number);return Double.valueOf(number);}
163
+ String string(){StringBuilder b=new StringBuilder();while(p<s.length()){char c=s.charAt(p++);if(c=='"')return b.toString();if(c=='\\'){c=s.charAt(p++);c=switch(c){case 'n'->'\n';case 'r'->'\r';case 't'->'\t';case 'b'->'\b';case 'f'->'\f';case 'u'->{char u=(char)Integer.parseInt(s.substring(p,p+4),16);p+=4;yield u;}case '"','\\','/'->c;default->throw fail("INVALID_ARGUMENT");};}else if(c<32)throw fail("INVALID_ARGUMENT");b.append(c);}throw fail("INVALID_ARGUMENT");}
164
+ static String write(Object o){if(o==null)return "null";if(o instanceof String s){StringBuilder b=new StringBuilder("\"");for(char c:s.toCharArray()){switch(c){case '"'->b.append("\\\"");case '\\'->b.append("\\\\");case '\n'->b.append("\\n");case '\r'->b.append("\\r");case '\t'->b.append("\\t");default->{if(c<32||Character.isSurrogate(c))b.append(String.format("\\u%04x",(int)c));else b.append(c);}}}return b.append('"').toString();}if(o instanceof Map<?,?>m){List<String>l=new ArrayList<>();m.forEach((k,v)->l.add(write(k.toString())+":"+write(v)));return "{"+String.join(",",l)+"}";}if(o instanceof Collection<?>l){List<String>a=new ArrayList<>();l.forEach(v->a.add(write(v)));return "["+String.join(",",a)+"]";}return o.toString();}
165
+ }
166
+ }
@@ -0,0 +1,37 @@
1
+ import { parseDiagnostic, type DebuggerDiagnostic } from './diagnostics.js'
2
+ /** Worker messages contain private debuggee data; never include them in errors. */
3
+ export interface WorkerEvent extends Record<string, unknown> {
4
+ event: string
5
+ reason?: string
6
+ diagnostic?: DebuggerDiagnostic
7
+ stop_id?: string
8
+ }
9
+ export type WorkerReply = Record<string, unknown>
10
+ export type WorkerMessage =
11
+ | { kind: 'event'; value: WorkerEvent }
12
+ | { kind: 'reply'; id: number; result: WorkerReply; error?: { code?: string; diagnostic?: DebuggerDiagnostic } }
13
+
14
+ const workerCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE', 'DEBUGGER_DISCONNECTED', 'DEBUGGER_WORKER_FAILED'])
15
+ function record(value: unknown): value is Record<string, unknown> {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
17
+ }
18
+ export function parseWorkerMessage(value: unknown): WorkerMessage {
19
+ if (!record(value)) throw new Error('INVALID_WORKER_MESSAGE')
20
+ if (value.event) {
21
+ if (typeof value.event !== 'string') throw new Error('INVALID_WORKER_EVENT')
22
+ for (const field of ['reason', 'stop_id']) {
23
+ if (value[field] !== undefined && typeof value[field] !== 'string') throw new Error('INVALID_WORKER_EVENT')
24
+ }
25
+ const event = { ...value } as WorkerEvent
26
+ if (value.diagnostic !== undefined) event.diagnostic = parseDiagnostic(value.diagnostic)
27
+ return { kind: 'event', value: event }
28
+ }
29
+ if (typeof value.id !== 'number' || !Number.isSafeInteger(value.id) || value.id < 1) throw new Error('INVALID_WORKER_REPLY')
30
+ if (value.error) {
31
+ if (!record(value.error) || (value.error.code !== undefined && typeof value.error.code !== 'string')) throw new Error('INVALID_WORKER_ERROR')
32
+ if (value.error.code !== undefined && !workerCodes.has(value.error.code as string)) throw new Error('INVALID_WORKER_ERROR')
33
+ return { kind: 'reply', id: value.id, result: {}, error: { ...(value.error.code === undefined ? {} : { code: value.error.code as string }), ...(value.error.diagnostic === undefined ? {} : { diagnostic: parseDiagnostic(value.error.diagnostic) }) } }
34
+ }
35
+ if (!record(value.result)) throw new Error('INVALID_WORKER_RESULT')
36
+ return { kind: 'reply', id: value.id, result: value.result }
37
+ }
@@ -0,0 +1,327 @@
1
+ import { atStage, classify, diagnostic, type DebuggerDiagnostic, type DiagnosticStage } from './diagnostics.js';
2
+ import type { WorkerReply } from './protocol.js';
3
+ import { registerDebuggerPauseProvider } from './guard.js';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { AndroidDebugNative, type DebugNative, type DebugTarget } from './android.js';
6
+ import { createWorker, prepareWorker, DebuggerError, withDebuggerBudget, debuggerRemaining, type DebugWorker, type WorkerEvent } from './worker.js';
7
+ type State = 'attaching' | 'running' | 'paused' | 'detaching' | 'detached' | 'disconnected' | 'failed';
8
+ interface Session {
9
+ id: string;
10
+ device: string;
11
+ target: DebugTarget | null;
12
+ state: State;
13
+ stop: WorkerEvent | null;
14
+ reason: string | null;
15
+ diagnostic?: DebuggerDiagnostic;
16
+ worker?: DebugWorker;
17
+ port?: number;
18
+ queue: Promise<unknown>;
19
+ events: SessionEvent[];
20
+ sequence: number;
21
+ cleanup: {
22
+ status: 'pending' | 'complete' | 'failed';
23
+ remaining_resources: string[];
24
+ };
25
+ terminalAt?: number;
26
+ created: number;
27
+ activity: number;
28
+ pausedAt?: number;
29
+ timer?: ReturnType<typeof setInterval>;
30
+ cleaning?: Promise<void>;
31
+ }
32
+ interface SessionEvent extends WorkerEvent {
33
+ sequence: number;
34
+ received_at_ms: number;
35
+ }
36
+ type CommandArgs = Record<string, unknown> & {
37
+ afterSequence?: number;
38
+ signal?: AbortSignal;
39
+ stopId?: string;
40
+ };
41
+ interface ResultPayload {
42
+ success?: boolean;
43
+ error?: {
44
+ code: string | null;
45
+ message: string | null;
46
+ retryable: boolean;
47
+ diagnostic?: DebuggerDiagnostic;
48
+ };
49
+ events?: SessionEvent[];
50
+ next_cursor?: number;
51
+ overflow?: boolean;
52
+ timed_out?: boolean;
53
+ execution_state?: 'unknown';
54
+ breakpoints?: unknown;
55
+ result?: WorkerReply;
56
+ }
57
+ export const debuggerCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
58
+ evaluation: false, mutation: false, coroutines: false, native: false };
59
+ const limitations = ['JAVA_KOTLIN_BYTECODE_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD'];
60
+ export function debuggerExpiry(created: number, activity: number, pausedAt: number | undefined, now: number) {
61
+ return pausedAt !== undefined && now - pausedAt >= 60000 ? 'pause_limit' : now - activity >= 300000 ? 'idle_limit' : now - created >= 1800000 ? 'session_limit' : null;
62
+ }
63
+ export class DebuggerSessions {
64
+ private sessions = new Map<string, Session>();
65
+ private closing = false;
66
+ constructor(private native: DebugNative = new AndroidDebugNative(), private workerFactory = createWorker, private prerequisite = prepareWorker) { }
67
+ private prune() {
68
+ const terminal = [...this.sessions.values()].filter(s => s.terminalAt !== undefined && s.cleanup.status === 'complete');
69
+ for (const s of terminal)
70
+ if (Date.now() - s.terminalAt! >= 600000)
71
+ this.sessions.delete(s.id);
72
+ const remaining = terminal.filter(s => this.sessions.has(s.id));
73
+ for (const s of remaining.slice(0, Math.max(0, remaining.length - 100)))
74
+ this.sessions.delete(s.id);
75
+ }
76
+ private get(id: string) { this.prune(); const s = this.sessions.get(id); if (!s)
77
+ throw new DebuggerError('DEBUGGER_NOT_FOUND'); s.activity = Date.now(); return s; }
78
+ private serial<T>(s: Session, fn: () => Promise<T>) { const out = s.queue.then(fn); s.queue = out.catch(() => { }); return out; }
79
+ private result(s: Session, payload: ResultPayload = {}) {
80
+ const success = payload.success ?? (!['failed', 'disconnected'].includes(s.state) && s.cleanup.status !== 'failed');
81
+ const code = s.cleanup.status === 'failed' ? 'DEBUGGER_CLEANUP_FAILED' : s.state === 'disconnected' ? 'DEBUGGER_DISCONNECTED' : s.reason?.match(/^[A-Z_]+$/) ? s.reason : 'DEBUGGER_WORKER_FAILED';
82
+ return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false, diagnostic: s.cleanup.status === 'failed' ? diagnostic('cleanup', 'operation_failed') : s.diagnostic ?? diagnostic('unknown', 'unknown') } } : {}), debug_session_id: s.id,
83
+ device: { platform: 'android', id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
84
+ capabilities: debuggerCapabilities, limitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload });
85
+ }
86
+ summary(id: string) { return this.result(this.get(id)); }
87
+ hasActiveSessions() { return [...this.sessions.values()].some(s => s.cleanup.status !== 'complete'); }
88
+ paused(device: string) { const s = [...this.sessions.values()].find(s => s.device === device && s.state === 'paused'); return s ? this.result(s) : null; }
89
+ assertRunning(device: string) { if (this.paused(device))
90
+ throw new DebuggerError('DEBUGGER_PAUSED'); }
91
+ private event(s: Session, event: WorkerEvent) {
92
+ if (s.cleanup.status === 'complete')
93
+ return;
94
+ if (event.event === 'stopped' && !s.cleaning) {
95
+ s.state = 'paused';
96
+ s.stop = event;
97
+ s.pausedAt ??= Date.now();
98
+ }
99
+ if (event.event === 'terminated' || event.event === 'worker_failed') {
100
+ if (!s.cleaning) s.diagnostic ??= event.diagnostic ?? diagnostic(event.event === 'worker_failed' ? 'ipc' : 'transport', event.reason === 'transport_lost' ? 'transport_lost' : 'unknown');
101
+ if (!s.cleaning && s.state !== 'failed')
102
+ s.reason = event.reason ?? 'worker_failed';
103
+ if (!s.cleaning) {
104
+ s.state = event.event === 'worker_failed' ? 'failed' : event.reason === 'process_exit' || event.reason === 'transport_lost' ? 'disconnected' : 'detached';
105
+ s.stop = null;
106
+ // The worker has disposed or lost its transport. Cleanup remains pending until host resources are checked.
107
+ void this.serial(s, () => this.cleanup(s, false));
108
+ }
109
+ }
110
+ s.events.push({ ...event, sequence: ++s.sequence, received_at_ms: Date.now() });
111
+ if (s.events.length > 256)
112
+ s.events.shift();
113
+ }
114
+ async capabilities(device: string, app: string, pid?: number) {
115
+ const prerequisites: Record<string, boolean> = { jdk: false, device: false, process: false };
116
+ let candidates: DebugTarget[] = [];
117
+ const reasons: string[] = [];
118
+ const diagnostics: DebuggerDiagnostic[] = [];
119
+ try {
120
+ await atStage('prerequisite', 'DEBUGGER_UNSUPPORTED', () => this.prerequisite());
121
+ prerequisites.jdk = true;
122
+ }
123
+ catch (e) {
124
+ if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
125
+ throw e;
126
+ reasons.push('DEBUGGER_UNSUPPORTED');
127
+ diagnostics.push(classify(e, 'prerequisite', 'DEBUGGER_UNSUPPORTED').diagnostic ?? diagnostic('prerequisite', 'unknown'));
128
+ }
129
+ try {
130
+ candidates = await atStage('target_resolution', 'DEVICE_UNAVAILABLE', () => this.native.candidates(device, app));
131
+ prerequisites.device = true;
132
+ prerequisites.process = candidates.some(t => pid === undefined || pid === t.pid);
133
+ if (!prerequisites.process)
134
+ { reasons.push('PROCESS_NOT_FOUND'); diagnostics.push(diagnostic('target_resolution', 'unavailable')); }
135
+ }
136
+ catch (e) {
137
+ if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
138
+ throw e;
139
+ reasons.push(e instanceof DebuggerError ? e.code : 'DEVICE_UNAVAILABLE');
140
+ diagnostics.push(classify(e, 'target_resolution', 'DEVICE_UNAVAILABLE').diagnostic ?? diagnostic('target_resolution', 'unknown'));
141
+ }
142
+ return { success: true, debug_session_id: null, device: { platform: 'android', id: device }, target: null, state: null,
143
+ captured_at_ms: Date.now(), supported: Object.values(prerequisites).every(Boolean), prerequisites, candidates,
144
+ ...(diagnostics.length ? { diagnostics } : {}), capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
145
+ }
146
+ async attach(device: string, app: string, pid?: number, timeout = 10000) {
147
+ this.prune();
148
+ const owned = [...this.sessions.values()].filter(s => s.cleanup.status !== 'complete');
149
+ if (this.closing || owned.length >= 4 || owned.some(s => s.device === device))
150
+ throw new DebuggerError('DEBUGGER_BUSY');
151
+ const s: Session = { id: randomUUID(), device, target: null, state: 'attaching', stop: null, reason: null,
152
+ queue: Promise.resolve(), events: [], sequence: 0, cleanup: { status: 'pending', remaining_resources: [] }, created: Date.now(), activity: Date.now() };
153
+ this.sessions.set(s.id, s);
154
+ return this.serial(s, async () => {
155
+ let stage: DiagnosticStage = 'prerequisite';
156
+ const deadline = Date.now() + timeout;
157
+ const remaining = () => { try { debuggerRemaining(timeout); const ms = deadline - Date.now(); if (ms <= 0)
158
+ throw new DebuggerError('DEBUGGER_TIMEOUT'); return ms; } catch (e) { throw classify(e, stage, 'DEBUGGER_ATTACH_FAILED'); } };
159
+ try {
160
+ await atStage('prerequisite', 'DEBUGGER_ATTACH_FAILED', () => this.prerequisite());
161
+ remaining();
162
+ stage = 'target_resolution';
163
+ const candidates = (await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.candidates(device, app))).filter(t => pid === undefined || t.pid === pid);
164
+ remaining();
165
+ if (!candidates.length)
166
+ throw new DebuggerError('PROCESS_NOT_FOUND', diagnostic('target_resolution', 'unavailable'));
167
+ if (candidates.length > 1)
168
+ throw new DebuggerError('PROCESS_AMBIGUOUS');
169
+ s.target = candidates[0];
170
+ stage = 'forwarding';
171
+ s.port = await atStage('forwarding', 'DEBUGGER_ATTACH_FAILED', () => this.native.forward(device, s.target!.pid));
172
+ remaining();
173
+ s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`);
174
+ stage = 'worker_start';
175
+ s.worker = await atStage('worker_start', 'DEBUGGER_ATTACH_FAILED', () => this.workerFactory(event => this.event(s, event)));
176
+ remaining();
177
+ s.cleanup.remaining_resources.push('jdi_worker');
178
+ stage = 'attach';
179
+ await atStage('attach', 'DEBUGGER_ATTACH_FAILED', () => s.worker!.call('attach', { port: s.port, timeoutMs: remaining() }, remaining()));
180
+ stage = 'target_resolution';
181
+ await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.identity(device, s.target!));
182
+ remaining();
183
+ if (s.state === 'attaching')
184
+ s.state = 'running';
185
+ if (['failed', 'disconnected', 'detached'].includes(s.state))
186
+ throw new DebuggerError('DEBUGGER_ATTACH_FAILED');
187
+ s.timer = setInterval(() => {
188
+ const now = Date.now();
189
+ const reason = debuggerExpiry(s.created, s.activity, s.pausedAt, now);
190
+ if (reason) {
191
+ s.reason = reason;
192
+ void this.serial(s, () => this.cleanup(s, true));
193
+ }
194
+ }, 1000);
195
+ s.timer.unref();
196
+ }
197
+ catch (e) {
198
+ s.diagnostic ??= classify(e, stage, 'DEBUGGER_ATTACH_FAILED').diagnostic;
199
+ s.state = 'failed';
200
+ s.reason = e instanceof DebuggerError ? e.code : 'DEBUGGER_ATTACH_FAILED';
201
+ await this.cleanup(s, true);
202
+ return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false, diagnostic: s.diagnostic } });
203
+ }
204
+ return this.result(s);
205
+ });
206
+ }
207
+ private async cleanup(s: Session, requestDetach: boolean): Promise<void> {
208
+ if (s.cleaning)
209
+ return s.cleaning;
210
+ s.cleaning = withDebuggerBudget(10000, undefined, async () => {
211
+ if (s.timer)
212
+ clearInterval(s.timer);
213
+ const terminal = ['failed', 'disconnected'].includes(s.state) ? s.state : 'detached';
214
+ s.state = 'detaching';
215
+ s.stop = null;
216
+ s.pausedAt = undefined;
217
+ const remaining: string[] = [];
218
+ if (s.worker) {
219
+ if (requestDetach) {
220
+ try {
221
+ await s.worker.call('detach', {}, 3000);
222
+ }
223
+ catch { /* EOF/transport closure is the fallback; execution remains unknown. */ }
224
+ }
225
+ try {
226
+ await s.worker.close();
227
+ s.worker = undefined;
228
+ }
229
+ catch {
230
+ remaining.push('jdi_worker');
231
+ }
232
+ }
233
+ if (s.port !== undefined && s.target) {
234
+ try {
235
+ await this.native.removeForward(s.device, s.port, s.target.pid);
236
+ s.port = undefined;
237
+ }
238
+ catch {
239
+ remaining.push(`forward:tcp:${s.port}`);
240
+ }
241
+ }
242
+ s.events = [];
243
+ s.cleanup = { status: remaining.length ? 'failed' : 'complete', remaining_resources: remaining };
244
+ s.state = terminal;
245
+ s.terminalAt = Date.now();
246
+ });
247
+ try {
248
+ await s.cleaning;
249
+ }
250
+ finally {
251
+ s.cleaning = undefined;
252
+ }
253
+ }
254
+ async command(id: string, command: string, args: CommandArgs = {}, timeout = 10000) {
255
+ const s = this.get(id);
256
+ if (command === 'events') {
257
+ const deadline = Date.now() + timeout;
258
+ const after = args.afterSequence ?? 0;
259
+ if (after > s.sequence)
260
+ throw new DebuggerError('INVALID_ARGUMENT');
261
+ if (args.signal?.aborted)
262
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
263
+ if (s.worker && ['running', 'paused'].includes(s.state))
264
+ await atStage('command', 'DEBUGGER_WORKER_FAILED', () => s.worker!.call('heartbeat', {}, Math.min(timeout, 1000)));
265
+ while (Date.now() < deadline && !s.events.some(e => e.sequence > after) && s.cleanup.status === 'pending') {
266
+ if (args.signal?.aborted)
267
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
268
+ await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
269
+ }
270
+ const events = s.events.filter(e => e.sequence > after).slice(0, 100);
271
+ return this.result(s, { events, next_cursor: events.at(-1)?.sequence ?? s.sequence, overflow: after < (s.events[0]?.sequence ?? s.sequence + 1) - 1, timed_out: events.length === 0 });
272
+ }
273
+ return this.serial(s, () => this.executeCommand(s, command, args, timeout));
274
+ }
275
+ private assertCommandTarget(s: Session, command: string, args: CommandArgs) {
276
+ if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))
277
+ throw new DebuggerError('DEBUGGER_DISCONNECTED', s.diagnostic ?? diagnostic('command', 'unavailable'));
278
+ if (['stack', 'locals', 'step', 'resume'].includes(command)) {
279
+ if (args.stopId !== s.stop?.stop_id)
280
+ throw new DebuggerError('DEBUGGER_STALE_REFERENCE');
281
+ if (s.state !== 'paused')
282
+ throw new DebuggerError('DEBUGGER_NOT_PAUSED');
283
+ }
284
+ }
285
+ private async executeCommand(s: Session, command: string, args: CommandArgs, timeout: number) {
286
+ if (command === 'detach') {
287
+ if (s.cleanup.status !== 'complete')
288
+ await this.cleanup(s, true);
289
+ return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' });
290
+ }
291
+ if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)))
292
+ return this.result(s);
293
+ this.assertCommandTarget(s, command, args);
294
+ if (command === 'step' || command === 'resume') {
295
+ s.state = 'running';
296
+ s.stop = null;
297
+ s.pausedAt = undefined;
298
+ }
299
+ try {
300
+ const data = await s.worker!.call(command, args, timeout);
301
+ return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
302
+ }
303
+ catch (e) {
304
+ return this.handleCommandError(s, command, classify(e, 'command', 'DEBUGGER_WORKER_FAILED'));
305
+ }
306
+ }
307
+ private async handleCommandError(s: Session, command: string, e: unknown) {
308
+ if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
309
+ s.diagnostic ??= e.diagnostic ?? diagnostic('transport', 'transport_lost');
310
+ s.state = 'disconnected';
311
+ s.reason = 'transport_lost';
312
+ await this.cleanup(s, false);
313
+ if (command === 'state')
314
+ return this.result(s);
315
+ }
316
+ if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
317
+ s.diagnostic ??= e.diagnostic;
318
+ s.state = 'failed';
319
+ s.reason = e.code;
320
+ await this.cleanup(s, true);
321
+ }
322
+ throw e;
323
+ }
324
+ async shutdown() { this.closing = true; await Promise.allSettled([...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.serial(s, () => this.cleanup(s, true)))); return [...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.result(s)); }
325
+ }
326
+ export const debuggerSessions = new DebuggerSessions();
327
+ registerDebuggerPauseProvider(device => debuggerSessions.paused(device));