mobile-debug-mcp 0.34.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 (432) 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 +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  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 +16 -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 +27 -2
  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 +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -1,168 +1,327 @@
1
- import { registerDebuggerPauseProvider } from './guard.js'
2
- import { randomUUID } from 'node:crypto'
3
- import { AndroidDebugNative, type DebugNative, type DebugTarget } from './android.js'
4
- import { createWorker, prepareWorker, DebuggerError, withDebuggerBudget, debuggerRemaining, type DebugWorker, type WorkerEvent } from './worker.js'
5
-
6
- type State = 'attaching' | 'running' | 'paused' | 'detaching' | 'detached' | 'disconnected' | 'failed'
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';
7
8
  interface Session {
8
- id: string; device: string; target: DebugTarget | null; state: State; stop: any; reason: string | null
9
- worker?: DebugWorker; port?: number; queue: Promise<unknown>; events: any[]; sequence: number
10
- cleanup: { status: 'pending' | 'complete' | 'failed'; remaining_resources: string[] }; terminalAt?: number
11
- created: number; activity: number; pausedAt?: number; timer?: ReturnType<typeof setInterval>; cleaning?: Promise<void>
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;
12
56
  }
13
57
  export const debuggerCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
14
- evaluation: false, mutation: false, coroutines: false, native: false }
15
- const limitations = ['JAVA_KOTLIN_BYTECODE_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD']
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'];
16
60
  export function debuggerExpiry(created: number, activity: number, pausedAt: number | undefined, now: number) {
17
- return pausedAt !== undefined && now - pausedAt >= 60000 ? 'pause_limit' : now - activity >= 300000 ? 'idle_limit' : now - created >= 1800000 ? 'session_limit' : null
61
+ return pausedAt !== undefined && now - pausedAt >= 60000 ? 'pause_limit' : now - activity >= 300000 ? 'idle_limit' : now - created >= 1800000 ? 'session_limit' : null;
18
62
  }
19
63
  export class DebuggerSessions {
20
- private sessions = new Map<string, Session>()
21
- private closing = false
22
- constructor(private native: DebugNative = new AndroidDebugNative(), private workerFactory = createWorker, private prerequisite = prepareWorker) {}
23
- private prune() {
24
- const terminal = [...this.sessions.values()].filter(s => s.terminalAt !== undefined && s.cleanup.status === 'complete')
25
- for (const s of terminal) if (Date.now() - s.terminalAt! >= 600000) this.sessions.delete(s.id)
26
- const remaining = terminal.filter(s => this.sessions.has(s.id))
27
- for (const s of remaining.slice(0, Math.max(0, remaining.length - 100))) this.sessions.delete(s.id)
28
- }
29
- private get(id: string) { this.prune(); const s = this.sessions.get(id); if (!s) throw new DebuggerError('DEBUGGER_NOT_FOUND'); s.activity = Date.now(); return s }
30
- private serial<T>(s: Session, fn: () => Promise<T>) { const out = s.queue.then(fn); s.queue = out.catch(() => {}); return out }
31
- private result(s: Session, payload: any = {}) {
32
- const success = payload.success ?? (!['failed', 'disconnected'].includes(s.state) && s.cleanup.status !== 'failed')
33
- const code = s.cleanup.status === 'failed' ? 'DEBUGGER_CLEANUP_FAILED' : s.state === 'disconnected' ? 'DEBUGGER_DISCONNECTED' : s.reason?.match(/^[A-Z_]+$/) ? s.reason : 'DEBUGGER_WORKER_FAILED'
34
- return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false } } : {}), debug_session_id: s.id,
35
- device: { platform: 'android', id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
36
- capabilities: debuggerCapabilities, limitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload })
37
- }
38
- summary(id: string) { return this.result(this.get(id)) }
39
- hasActiveSessions() { return [...this.sessions.values()].some(s => s.cleanup.status !== 'complete') }
40
- paused(device: string) { const s = [...this.sessions.values()].find(s => s.device === device && s.state === 'paused'); return s ? this.result(s) : null }
41
- assertRunning(device: string) { if (this.paused(device)) throw new DebuggerError('DEBUGGER_PAUSED') }
42
- private event(s: Session, event: WorkerEvent) {
43
- if (s.cleanup.status === 'complete') return
44
- if (event.event === 'stopped' && !s.cleaning) { s.state = 'paused'; s.stop = event; s.pausedAt ??= Date.now() }
45
- if (event.event === 'terminated' || event.event === 'worker_failed') {
46
- if (!s.cleaning && s.state !== 'failed') s.reason = event.reason ?? 'worker_failed'
47
- if (!s.cleaning) {
48
- s.state = event.event === 'worker_failed' ? 'failed' : event.reason === 'process_exit' || event.reason === 'transport_lost' ? 'disconnected' : 'detached'
49
- s.stop = null
50
- // The worker has disposed or lost its transport. Cleanup remains pending until host resources are checked.
51
- void this.serial(s, () => this.cleanup(s, false))
52
- }
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
+ });
53
206
  }
54
- s.events.push({ ...event, sequence: ++s.sequence, received_at_ms: Date.now() })
55
- if (s.events.length > 256) s.events.shift()
56
- }
57
- async capabilities(device: string, app: string, pid?: number) {
58
- const prerequisites: Record<string, boolean> = { jdk: false, device: false, process: false }
59
- let candidates: DebugTarget[] = []
60
- const reasons: string[] = []
61
- try { await this.prerequisite(); prerequisites.jdk = true } catch (e) { if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code)) throw e; reasons.push('DEBUGGER_UNSUPPORTED') }
62
- try { candidates = await this.native.candidates(device, app); prerequisites.device = true; prerequisites.process = candidates.some(t => pid === undefined || pid === t.pid); if (!prerequisites.process) reasons.push('PROCESS_NOT_FOUND') }
63
- catch (e) { if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code)) throw e; reasons.push(e instanceof DebuggerError ? e.code : 'DEVICE_UNAVAILABLE') }
64
- return { success: true, debug_session_id: null, device: { platform: 'android', id: device }, target: null, state: null,
65
- captured_at_ms: Date.now(), supported: Object.values(prerequisites).every(Boolean), prerequisites, candidates,
66
- capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } }
67
- }
68
- async attach(device: string, app: string, pid?: number, timeout = 10000) {
69
- this.prune()
70
- const owned = [...this.sessions.values()].filter(s => s.cleanup.status !== 'complete')
71
- if (this.closing || owned.length >= 4 || owned.some(s => s.device === device)) throw new DebuggerError('DEBUGGER_BUSY')
72
- const s: Session = { id: randomUUID(), device, target: null, state: 'attaching', stop: null, reason: null,
73
- queue: Promise.resolve(), events: [], sequence: 0, cleanup: { status: 'pending', remaining_resources: [] }, created: Date.now(), activity: Date.now() }
74
- this.sessions.set(s.id, s)
75
- return this.serial(s, async () => {
76
- const deadline = Date.now() + timeout
77
- const remaining = () => { debuggerRemaining(timeout); const ms = deadline - Date.now(); if (ms <= 0) throw new DebuggerError('DEBUGGER_TIMEOUT'); return ms }
78
- try {
79
- await this.prerequisite(); remaining()
80
- const candidates = (await this.native.candidates(device, app)).filter(t => pid === undefined || t.pid === pid); remaining()
81
- if (!candidates.length) throw new DebuggerError('PROCESS_NOT_FOUND')
82
- if (candidates.length > 1) throw new DebuggerError('PROCESS_AMBIGUOUS')
83
- s.target = candidates[0]
84
- s.port = await this.native.forward(device, s.target.pid); remaining()
85
- s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`)
86
- s.worker = await this.workerFactory(event => this.event(s, event)); remaining()
87
- s.cleanup.remaining_resources.push('jdi_worker')
88
- await s.worker.call('attach', { port: s.port, timeoutMs: remaining() }, remaining())
89
- await this.native.identity(device, s.target); remaining()
90
- if (s.state === 'attaching') s.state = 'running'
91
- if (['failed', 'disconnected', 'detached'].includes(s.state)) throw new DebuggerError('DEBUGGER_ATTACH_FAILED')
92
- s.timer = setInterval(() => {
93
- const now = Date.now()
94
- const reason = debuggerExpiry(s.created, s.activity, s.pausedAt, now)
95
- if (reason) { s.reason = reason; void this.serial(s, () => this.cleanup(s, true)) }
96
- }, 1000); s.timer.unref()
97
- } catch (e) {
98
- s.state = 'failed'; s.reason = e instanceof DebuggerError ? e.code : 'DEBUGGER_ATTACH_FAILED'
99
- await this.cleanup(s, true)
100
- return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false } })
101
- }
102
- return this.result(s)
103
- })
104
- }
105
- private async cleanup(s: Session, requestDetach: boolean): Promise<void> {
106
- if (s.cleaning) return s.cleaning
107
- s.cleaning = withDebuggerBudget(10000, undefined, async () => {
108
- if (s.timer) clearInterval(s.timer)
109
- const terminal = ['failed', 'disconnected'].includes(s.state) ? s.state : 'detached'
110
- s.state = 'detaching'; s.stop = null; s.pausedAt = undefined
111
- const remaining: string[] = []
112
- if (s.worker) {
113
- if (requestDetach) { try { await s.worker.call('detach', {}, 3000) } catch { /* EOF/transport closure is the fallback; execution remains unknown. */ } }
114
- try { await s.worker.close(); s.worker = undefined } catch { remaining.push('jdi_worker') }
115
- }
116
- if (s.port !== undefined && s.target) {
117
- try { await this.native.removeForward(s.device, s.port, s.target.pid); s.port = undefined }
118
- catch { remaining.push(`forward:tcp:${s.port}`) }
119
- }
120
- s.events = []; s.cleanup = { status: remaining.length ? 'failed' : 'complete', remaining_resources: remaining }
121
- s.state = terminal; s.terminalAt = Date.now()
122
- })
123
- try { await s.cleaning } finally { s.cleaning = undefined }
124
- }
125
- async command(id: string, command: string, args: Record<string, any> = {}, timeout = 10000) {
126
- const s = this.get(id)
127
- if (command === 'events') {
128
- const deadline = Date.now() + timeout
129
- const after = args.afterSequence ?? 0
130
- if (after > s.sequence) throw new DebuggerError('INVALID_ARGUMENT')
131
- if (args.signal?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
132
- if (s.worker && ['running', 'paused'].includes(s.state)) await s.worker.call('heartbeat', {}, Math.min(timeout, 1000))
133
- while (Date.now() < deadline && !s.events.some(e => e.sequence > after) && s.cleanup.status === 'pending') {
134
- if (args.signal?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
135
- await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())))
136
- }
137
- const events = s.events.filter(e => e.sequence > after).slice(0, 100)
138
- 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 })
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
+ }
139
284
  }
140
- return this.serial(s, async () => {
141
- if (command === 'detach') { if (s.cleanup.status !== 'complete') await this.cleanup(s, true); return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' }) }
142
- if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))) return this.result(s)
143
- if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)) throw new DebuggerError('DEBUGGER_DISCONNECTED')
144
- if (['stack', 'locals', 'step', 'resume'].includes(command)) {
145
- if (args.stopId !== s.stop?.stop_id) throw new DebuggerError('DEBUGGER_STALE_REFERENCE')
146
- if (s.state !== 'paused') throw new DebuggerError('DEBUGGER_NOT_PAUSED')
147
- }
148
- if (command === 'step' || command === 'resume') { s.state = 'running'; s.stop = null; s.pausedAt = undefined }
149
- try {
150
- const data = await s.worker.call(command, args, timeout)
151
- return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data })
152
- } catch (e) {
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) {
153
308
  if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
154
- s.state = 'disconnected'; s.reason = 'transport_lost'; await this.cleanup(s, false)
155
- if (command === 'state') return this.result(s)
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);
156
315
  }
157
316
  if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
158
- s.state = 'failed'; s.reason = e.code; await this.cleanup(s, true)
317
+ s.diagnostic ??= e.diagnostic;
318
+ s.state = 'failed';
319
+ s.reason = e.code;
320
+ await this.cleanup(s, true);
159
321
  }
160
- throw e
161
- }
162
- })
163
- }
164
- 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)) }
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)); }
165
325
  }
166
- export const debuggerSessions = new DebuggerSessions()
167
-
168
- registerDebuggerPauseProvider(device => debuggerSessions.paused(device))
326
+ export const debuggerSessions = new DebuggerSessions();
327
+ registerDebuggerPauseProvider(device => debuggerSessions.paused(device));