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,133 @@
1
+ import { execCmd } from '../utils/exec.js'
2
+ import { getAdbCmd } from '../utils/android/utils.js'
3
+
4
+ export const TRACE_MAX_BYTES = 32 * 1024 * 1024
5
+ export const TRACE_CATEGORIES = ['sched', 'freq', 'idle', 'am', 'wm', 'gfx', 'view', 'input']
6
+ export interface TraceCapabilities {
7
+ supported: boolean; backend: 'perfetto'; device: { platform: string; id: string } | null
8
+ prerequisites: Record<string, boolean>; reasons: string[]; enabled_sources: string[]; unavailable_categories: string[]; limitations: string[]
9
+ }
10
+ export interface TraceProcess { pid: number; startTime: string }
11
+ export interface TraceNative {
12
+ capabilities(deviceId: string): Promise<TraceCapabilities>
13
+ start(deviceId: string, remote: string, duration: number, categories: string[]): Promise<void>
14
+ find(deviceId: string, remote: string): Promise<TraceProcess | null>
15
+ stop(deviceId: string, remote: string, owner: TraceProcess): Promise<void>
16
+ pull(deviceId: string, remote: string, local: string): Promise<void>
17
+ remove(deviceId: string, remote: string): Promise<void>
18
+ }
19
+
20
+ // Encode only this fixed preset using the public Perfetto proto field numbers:
21
+ // https://github.com/google/perfetto/tree/main/protos/perfetto/config
22
+ // Avoid --txt (explicitly unstable), arbitrary configuration and extra tooling.
23
+ function varint(n: number): Buffer {
24
+ const bytes: number[] = []
25
+ do { const byte = n % 128; n = Math.floor(n / 128); bytes.push(byte | (n ? 128 : 0)) } while (n)
26
+ return Buffer.from(bytes)
27
+ }
28
+ const uint = (field: number, n: number) => Buffer.concat([varint(field * 8), varint(n)])
29
+ const message = (field: number, value: Buffer | string) => {
30
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value)
31
+ return Buffer.concat([varint(field * 8 + 2), varint(bytes.length), bytes])
32
+ }
33
+ export function traceConfig(duration: number, categories: string[]): Buffer {
34
+ const ftrace = Buffer.concat(categories.map(cat => message(2, cat)))
35
+ const source = Buffer.concat([message(1, 'linux.ftrace'), message(100, ftrace)])
36
+ return Buffer.concat([
37
+ message(1, Buffer.concat([uint(1, 16384), uint(4, 1)])),
38
+ message(2, message(1, source)), uint(3, duration), uint(8, 1), uint(10, TRACE_MAX_BYTES)
39
+ ])
40
+ }
41
+ const quote = (value: string) => `'${value.replace(/'/g, `'"'"'`)}'`
42
+ function ownedPath(remote: string) {
43
+ if (!/^\/data\/misc\/perfetto-traces\/mcp-[0-9a-f-]{36}\.perfetto-trace$/.test(remote)) throw new Error('Invalid owned trace path')
44
+ }
45
+
46
+ export class AndroidTraceNative implements TraceNative {
47
+ constructor(private execute: typeof execCmd = execCmd) {}
48
+ private async command(device: string, args: string[], timeout = 10000) {
49
+ const result = await this.execute(getAdbCmd(), ['-s', device, ...args], { timeout })
50
+ if (result.exitCode !== 0) throw new Error(result.stderr === 'ACTION_TIMEOUT' ? 'ACTION_TIMEOUT' : 'ADB_TRACE_COMMAND_FAILED')
51
+ return result.stdout
52
+ }
53
+ private shell(device: string, script: string, timeout?: number) {
54
+ return this.command(device, ['shell', 'sh', '-c', quote(script)], timeout)
55
+ }
56
+ async capabilities(deviceId: string): Promise<TraceCapabilities> {
57
+ const checks: Record<string, boolean> = {}
58
+ const probe = async (name: string, fn: () => Promise<boolean>) => { try { checks[name] = await fn() } catch { checks[name] = false } }
59
+ let help = '', categories = ''
60
+ await probe('android_api_29', async () => Number(await this.command(deviceId, ['shell', 'getprop', 'ro.build.version.sdk'])) >= 29)
61
+ await probe('perfetto', async () => { const result = await this.execute(getAdbCmd(), ['-s', deviceId, 'shell', 'perfetto', '--help'], { timeout: 10000 }); help = result.stdout + result.stderr; return [0, 1].includes(result.exitCode!) && help.includes('Usage: perfetto') && help.includes('--background-wait') && help.includes('--config') })
62
+ await probe('tracing_service', async () => { await this.command(deviceId, ['shell', 'perfetto', '--query']); return true })
63
+ await probe('writable_storage', async () => { await this.shell(deviceId, 'test -w /data/misc/perfetto-traces'); return true })
64
+ await probe('process_identity', async () => { await this.shell(deviceId, 'command -v pidof >/dev/null && test -r /proc/self/stat && test -r /proc/self/cmdline'); return true })
65
+ await probe('base64', async () => (await this.shell(deviceId, "printf YQ== | base64 -d")) === 'a')
66
+ await probe('required_categories', async () => {
67
+ categories = await this.command(deviceId, ['shell', 'atrace', '--list_categories'])
68
+ return ['sched', 'input'].every(c => new RegExp(`^\\s*${c}\\s+-`, 'm').test(categories))
69
+ })
70
+ const enabled = TRACE_CATEGORIES.filter(c => new RegExp(`^\\s*${c}\\s+-`, 'm').test(categories))
71
+ return { supported: Object.values(checks).every(Boolean), backend: 'perfetto', device: { platform: 'android', id: deviceId },
72
+ prerequisites: checks, reasons: Object.keys(checks).filter(k => !checks[k]), enabled_sources: enabled,
73
+ unavailable_categories: TRACE_CATEGORIES.filter(c => !enabled.includes(c)), limitations: [
74
+ 'Device-wide scheduling and system annotations may contain sensitive information.',
75
+ 'Host times are not aligned with device trace clocks; source availability varies.',
76
+ 'A server crash can leave a bounded trace file on the device for manual removal.'
77
+ ] }
78
+ }
79
+ async start(device: string, remote: string, duration: number, categories: string[]) {
80
+ ownedPath(remote)
81
+ const encoded = traceConfig(duration, categories).toString('base64')
82
+ await this.shell(device, `umask 077; printf %s ${quote(encoded)} | base64 -d | perfetto --background-wait -c - -o ${quote(remote)}`, 35000)
83
+ }
84
+ // Scan only shell-owned readable processes. Exact output argv + executable name
85
+ // binds ownership even if start acknowledgement/PID output was lost.
86
+ async find(device: string, remote: string): Promise<TraceProcess | null> {
87
+ ownedPath(remote)
88
+ const output = await this.shell(device, `pids=$(pidof perfetto); rc=$?
89
+ [ "$rc" -le 1 ] || exit 1
90
+ for pid in $pids; do
91
+ p=/proc/$pid
92
+ [ -d "$p" ] || continue
93
+ [ -r "$p/cmdline" ] || { [ ! -d "$p" ] && continue; exit 1; }
94
+ cmd=$(tr '\\000' '\\n' < "$p/cmdline" 2>/dev/null) || { [ ! -d "$p" ] && continue; exit 1; }
95
+ printf '%s\\n' "$cmd" | grep -Fx ${quote(remote)} >/dev/null || continue
96
+ first=$(printf '%s\\n' "$cmd" | head -n 1)
97
+ case "$first" in perfetto|/system/bin/perfetto) ;; *) continue ;; esac
98
+ stat=$(cat "$p/stat" 2>/dev/null) || { [ ! -d "$p" ] && continue; exit 1; }
99
+ rest=\${stat##*) }
100
+ state=$(printf '%s\\n' "$rest" | awk '{print $1}')
101
+ [ "$state" = Z ] && continue
102
+ start=$(printf '%s\\n' "$rest" | awk '{print $20}')
103
+ printf '%s %s\\n' "\${p##*/}" "$start"
104
+ done`)
105
+ if (!output) return null
106
+ const match = /^(\d+) (\d+)$/.exec(output)
107
+ if (!match) throw new Error('TRACE_PROCESS_IDENTITY_UNAVAILABLE')
108
+ return { pid: Number(match[1]), startTime: match[2] }
109
+ }
110
+ async stop(device: string, remote: string, owner: TraceProcess) {
111
+ ownedPath(remote)
112
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 1 || !/^\d+$/.test(owner.startTime)) throw new Error('TRACE_PROCESS_IDENTITY_UNAVAILABLE')
113
+ // Recheck start time and argv in the same shell immediately before signal.
114
+ await this.shell(device, `p=/proc/${owner.pid}
115
+ [ -d "$p" ] || exit 0
116
+ stat=$(cat "$p/stat") || exit 1
117
+ rest=\${stat##*) }
118
+ [ "$(printf '%s\\n' "$rest" | awk '{print $20}')" = ${quote(owner.startTime)} ] || exit 1
119
+ cmd=$(tr '\\000' '\\n' < "$p/cmdline") || exit 1
120
+ first=$(printf '%s\\n' "$cmd" | head -n 1)
121
+ case "$first" in perfetto|/system/bin/perfetto) ;; *) exit 1 ;; esac
122
+ printf '%s\\n' "$cmd" | grep -Fx ${quote(remote)} >/dev/null || exit 1
123
+ kill -TERM ${owner.pid}`)
124
+ }
125
+ async pull(device: string, remote: string, local: string) {
126
+ ownedPath(remote)
127
+ // Reject oversized output before initiating transfer as well as afterwards.
128
+ const size = Number(await this.command(device, ['shell', 'stat', '-c', '%s', remote]))
129
+ if (!Number.isSafeInteger(size) || size <= 0 || size > TRACE_MAX_BYTES) throw new Error('TRACE_ARTIFACT_SIZE_INVALID')
130
+ await this.command(device, ['pull', remote, local], 30000)
131
+ }
132
+ async remove(device: string, remote: string) { ownedPath(remote); await this.command(device, ['shell', 'rm', '-f', remote]) }
133
+ }
@@ -0,0 +1,227 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { promises as fs } from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { AndroidTraceNative, TRACE_MAX_BYTES, type TraceCapabilities, type TraceNative, type TraceProcess } from './system-trace-android.js'
6
+ import { evidenceSessionId } from './evidence.js'
7
+ import { runWithBudget, remainingBudget } from '../utils/operation-budget.js'
8
+
9
+ export class TraceError extends Error {
10
+ constructor(public code: string) { super(code) }
11
+ }
12
+ type State = 'starting' | 'recording' | 'stopping' | 'completed' | 'cancelled' | 'failed'
13
+ type Completion = 'requested' | 'duration_limit' | 'cancelled' | 'error'
14
+ export interface TraceResult {
15
+ trace_id: string; session_id: string; device: { platform: 'android'; id: string }; backend: 'perfetto'
16
+ state: State; reason: string | null; completion_reason: Completion | null
17
+ capture_started_at_ms: number | null; capture_completed_at_ms: number | null
18
+ artifact: { path: string; format: 'perfetto'; size_bytes: number } | null
19
+ cleanup: { status: 'pending' | 'complete' | 'failed'; remaining_resources: string[] }
20
+ correlation_ids: string[]; enabled_sources: string[]; unavailable_categories: string[]
21
+ }
22
+ interface Session {
23
+ result: TraceResult; remote: string; owner: TraceProcess | null; stopped: boolean; discard: boolean
24
+ timer?: ReturnType<typeof setTimeout>; terminalAt?: number; deadline: number; local?: string
25
+ queue: Promise<unknown>; linked: Set<string>; partial: Set<string>
26
+ }
27
+ export interface TraceClock {
28
+ wall(): number; mono(): number; sleep(ms: number): Promise<void>
29
+ schedule(fn: () => void, ms: number): ReturnType<typeof setTimeout>
30
+ clear(timer: ReturnType<typeof setTimeout>): void
31
+ }
32
+ const clock: TraceClock = {
33
+ wall: Date.now, mono: () => performance.now(), sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
34
+ schedule: (fn, ms) => { const t = setTimeout(fn, ms); t.unref(); return t }, clear: clearTimeout
35
+ }
36
+
37
+ export class SystemTraceSessions {
38
+ private sessions = new Map<string, Session>()
39
+ private closing = false
40
+ constructor(private native: TraceNative = new AndroidTraceNative(), private time: TraceClock = clock) { }
41
+ private prune() {
42
+ const terminal = [...this.sessions.values()].filter(s => s.terminalAt !== undefined && s.result.cleanup.status === 'complete')
43
+ for (const s of terminal) if (this.time.mono() - s.terminalAt! >= 600000) this.sessions.delete(s.result.trace_id)
44
+ const remaining = terminal.filter(s => this.sessions.has(s.result.trace_id))
45
+ for (const s of remaining.slice(0, Math.max(0, remaining.length - 100))) this.sessions.delete(s.result.trace_id)
46
+ }
47
+
48
+ private get(id: string) { this.prune(); const s = this.sessions.get(id); if (!s) throw new TraceError('TRACE_NOT_FOUND'); return s }
49
+ private snapshot(s: Session): TraceResult { return structuredClone(s.result) }
50
+ private serial<T>(s: Session, fn: () => Promise<T>): Promise<T> {
51
+ const result = s.queue.then(fn)
52
+ s.queue = result.catch(() => { })
53
+ return result
54
+ }
55
+ async capabilities(device: string) { return this.native.capabilities(device) }
56
+ async start(device: string, duration = 30000): Promise<TraceResult> {
57
+ if (!Number.isInteger(duration) || duration < 1000 || duration > 120000) throw new TraceError('INVALID_ARGUMENT')
58
+ this.prune()
59
+ const reserved = [...this.sessions.values()].filter(s => s.result.cleanup.status !== 'complete')
60
+ if (this.closing || reserved.length >= 4 || reserved.some(s => s.result.device.id === device)) throw new TraceError('TRACE_BUSY')
61
+ const id = randomUUID()
62
+ const result: TraceResult = {
63
+ trace_id: id, session_id: evidenceSessionId(), device: { platform: 'android', id: device }, backend: 'perfetto',
64
+ state: 'starting', reason: null, completion_reason: null, capture_started_at_ms: null, capture_completed_at_ms: null,
65
+ artifact: null, cleanup: { status: 'pending', remaining_resources: [] }, correlation_ids: [], enabled_sources: [], unavailable_categories: []
66
+ }
67
+ const s: Session = {
68
+ result, remote: `/data/misc/perfetto-traces/mcp-${id}.perfetto-trace`, owner: null, stopped: true,
69
+ discard: false, deadline: Infinity, queue: Promise.resolve(), linked: new Set(), partial: new Set()
70
+ }
71
+ this.sessions.set(id, s)
72
+ return this.serial(s, async () => {
73
+ let cap: TraceCapabilities
74
+ try { cap = await this.native.capabilities(device) }
75
+ catch { this.fail(s, 'TRACE_UNSUPPORTED'); this.clean(s); return this.snapshot(s) }
76
+ s.result.enabled_sources = cap.enabled_sources
77
+ s.result.unavailable_categories = cap.unavailable_categories
78
+ if (!cap.supported || this.closing) { this.fail(s, this.closing ? 'TRACE_BUSY' : 'TRACE_UNSUPPORTED'); this.clean(s); return this.snapshot(s) }
79
+ s.stopped = false
80
+ s.result.cleanup.remaining_resources = [s.remote, 'device_capture']
81
+ // Conservative host bounds include acknowledgement latency, not exact device start.
82
+ s.result.capture_started_at_ms = this.time.wall()
83
+ s.deadline = this.time.mono() + duration
84
+ try {
85
+ await this.native.start(device, s.remote, duration, cap.enabled_sources)
86
+ s.owner = await this.native.find(device, s.remote)
87
+ s.result.state = 'recording'
88
+ s.timer = this.time.schedule(() => { void this.control(id, 'duration').catch(() => { }) }, Math.max(0, s.deadline - this.time.mono()))
89
+ } catch (error) {
90
+ s.discard = true
91
+ this.fail(s, this.code(error, 'TRACE_START_FAILED'))
92
+ try { await this.halt(s); await this.remove(s) } catch { s.result.cleanup.status = 'failed' }
93
+ }
94
+ return this.snapshot(s)
95
+ })
96
+ }
97
+ private code(error: unknown, fallback: string) { return error instanceof Error && error.message === 'ACTION_TIMEOUT' ? 'ACTION_TIMEOUT' : fallback }
98
+ private fail(s: Session, reason: string) {
99
+ s.result.state = 'failed'; s.result.reason = reason; s.result.completion_reason = 'error'
100
+ s.terminalAt = this.time.mono()
101
+ }
102
+ private clean(s: Session) {
103
+ s.result.cleanup = { status: 'complete', remaining_resources: [] }
104
+ s.terminalAt = this.time.mono()
105
+ this.prune()
106
+ }
107
+ private async halt(s: Session) {
108
+ if (s.stopped) return
109
+ const deadline = this.time.mono() + 10000
110
+ await runWithBudget(10000, async () => {
111
+ const found = await this.native.find(s.result.device.id, s.remote)
112
+ if (found) {
113
+ if (s.owner && (s.owner.pid !== found.pid || s.owner.startTime !== found.startTime)) throw new TraceError('TRACE_STOP_FAILED')
114
+ s.owner ??= found
115
+ try { await this.native.stop(s.result.device.id, s.remote, s.owner) }
116
+ catch (error) {
117
+ // Natural expiry can win between identity lookup and SIGTERM. Only
118
+ // confirmed absence permits finalisation; unreadable/reused identity fails.
119
+ if (await this.native.find(s.result.device.id, s.remote)) throw error
120
+ }
121
+ while (await this.native.find(s.result.device.id, s.remote)) {
122
+ if (this.time.mono() >= deadline) throw new TraceError('ACTION_TIMEOUT')
123
+ remainingBudget(); await this.time.sleep(100)
124
+ }
125
+ }
126
+ })
127
+ s.stopped = true
128
+ s.result.capture_completed_at_ms = this.time.wall()
129
+ s.result.cleanup.remaining_resources = [s.remote]
130
+ for (const linked of s.linked) s.partial.add(linked)
131
+ }
132
+ private async remove(s: Session) {
133
+ if (!s.stopped) throw new TraceError('TRACE_STOP_FAILED')
134
+ await this.native.remove(s.result.device.id, s.remote)
135
+ if (s.local && !s.result.artifact) { await fs.rm(s.local, { recursive: true, force: true }); s.local = undefined }
136
+ this.clean(s)
137
+ }
138
+ private async artifact(s: Session) {
139
+ if (s.result.artifact) return
140
+ s.local ??= await fs.mkdtemp(path.join(os.tmpdir(), 'mobile-debug-trace-'))
141
+ const local = s.local
142
+ const partial = path.join(local, 'capture.partial')
143
+ try {
144
+ await fs.chmod(s.local, 0o700)
145
+ await this.native.pull(s.result.device.id, s.remote, partial)
146
+ const stat = await fs.stat(partial)
147
+ if (!stat.isFile() || stat.size <= 0 || stat.size > TRACE_MAX_BYTES) throw new TraceError('TRACE_ARTIFACT_FAILED')
148
+ await fs.chmod(partial, 0o600)
149
+ const target = path.join(s.local, `${s.result.trace_id}.perfetto-trace`)
150
+ await fs.rename(partial, target)
151
+ s.result.artifact = { path: target, format: 'perfetto', size_bytes: stat.size }
152
+ } catch (error) {
153
+ try { await fs.rm(local, { recursive: true, force: true }); s.local = undefined }
154
+ catch { s.result.cleanup.remaining_resources = [s.remote, local] }
155
+ throw error
156
+ }
157
+ }
158
+ async control(id: string, operation: 'status' | 'stop' | 'cancel' | 'duration'): Promise<TraceResult> {
159
+ const s = this.get(id)
160
+ return this.serial(s, async () => {
161
+ if (s.result.cleanup.status === 'complete') return this.snapshot(s)
162
+ if (operation === 'status' && s.result.state === 'recording') {
163
+ try {
164
+ if (await this.native.find(s.result.device.id, s.remote) && this.time.mono() < s.deadline) return this.snapshot(s)
165
+ } catch (error) { this.fail(s, this.code(error, 'TRACE_STOP_FAILED')); s.result.cleanup.status = 'failed'; return this.snapshot(s) }
166
+ }
167
+ if (s.timer) { this.time.clear(s.timer); s.timer = undefined }
168
+ if (operation === 'cancel') s.discard = true
169
+ const hadStartFailure = s.discard && s.result.state === 'failed' && operation !== 'cancel'
170
+ s.result.state = 'stopping'
171
+ s.result.cleanup.status = 'pending'
172
+ try { await this.halt(s) }
173
+ catch (error) { this.fail(s, this.code(error, 'TRACE_STOP_FAILED')); s.result.cleanup.status = 'failed'; return this.snapshot(s) }
174
+ if (!s.discard) {
175
+ try { await this.artifact(s) }
176
+ catch (error) { this.fail(s, this.code(error, 'TRACE_ARTIFACT_FAILED')); s.result.cleanup.status = 'failed'; return this.snapshot(s) }
177
+ }
178
+ try { await this.remove(s) }
179
+ catch { this.fail(s, 'TRACE_STOP_FAILED'); s.result.cleanup.status = 'failed'; return this.snapshot(s) }
180
+ this.complete(s, hadStartFailure, operation)
181
+ return this.snapshot(s)
182
+ })
183
+ }
184
+ private complete(s: Session, hadStartFailure: boolean, operation: string) {
185
+ if (hadStartFailure) s.result.state = 'failed'
186
+ else {
187
+ s.result.state = s.discard ? 'cancelled' : 'completed'
188
+ s.result.reason = s.discard ? 'TRACE_CANCELLED' : null
189
+ s.result.completion_reason = s.discard ? 'cancelled' : operation === 'duration' || this.time.mono() >= s.deadline ? 'duration_limit' : 'requested'
190
+ }
191
+ }
192
+ async link(id: string, device: { platform: string; id: string }, correlation: string) {
193
+ const s = this.get(id)
194
+ if (device.platform !== 'android' || device.id !== s.result.device.id) throw new TraceError('TRACE_NOT_FOUND')
195
+ const status = await this.control(id, 'status')
196
+ if (device.platform !== 'android' || device.id !== status.device.id || status.state !== 'recording') throw new TraceError('TRACE_NOT_FOUND')
197
+ // Validate again after the asynchronous status call: stop/cancel may have won.
198
+ return this.serial(s, async () => {
199
+ if (s.result.state !== 'recording' || this.time.mono() >= s.deadline) throw new TraceError('TRACE_NOT_FOUND')
200
+ if (!s.result.correlation_ids.includes(correlation)) {
201
+ if (s.result.correlation_ids.length >= 1000) throw new TraceError('TRACE_BUSY')
202
+ s.result.correlation_ids.push(correlation)
203
+ }
204
+ s.linked.add(correlation)
205
+ let finish: Promise<{ trace_id: string; coverage: string; clock_alignment: string }> | undefined
206
+ return () => finish ??= (async () => {
207
+ let recording = false
208
+ try { recording = (await this.control(id, 'status')).state === 'recording' }
209
+ catch { /* Unavailable state cannot establish complete coverage. */ }
210
+ s.linked.delete(correlation)
211
+ return {
212
+ trace_id: id, coverage: !recording || s.partial.has(correlation) || this.time.mono() >= s.deadline ? 'partial' : 'covered',
213
+ clock_alignment: 'unknown'
214
+ }
215
+ })()
216
+ })
217
+ }
218
+ async shutdown() {
219
+ this.closing = true
220
+ // Independent device cleanup in parallel under one overall deadline.
221
+ return runWithBudget(10000, async () => {
222
+ await Promise.allSettled([...this.sessions.values()].filter(s => s.result.cleanup.status !== 'complete').map(s => this.control(s.result.trace_id, 'cancel')))
223
+ return [...this.sessions.values()].filter(s => s.result.cleanup.status !== 'complete').map(s => this.snapshot(s))
224
+ })
225
+ }
226
+ }
227
+ export const systemTraces = new SystemTraceSessions()