mobile-debug-mcp 0.32.4 → 0.34.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 (170) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +14 -0
  3. package/.cursor-plugin/marketplace.json +16 -0
  4. package/.github/copilot-instructions.md +7 -0
  5. package/.github/workflows/ci.yml +21 -0
  6. package/AGENTS.md +21 -10
  7. package/README.md +69 -4
  8. package/agent-plugin/.claude-plugin/plugin.json +20 -0
  9. package/agent-plugin/.codex-plugin/plugin.json +34 -0
  10. package/agent-plugin/.cursor-plugin/plugin.json +23 -0
  11. package/agent-plugin/.mcp.json +14 -0
  12. package/agent-plugin/README.md +9 -3
  13. package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
  14. package/agent-plugin/mcp.json +1 -1
  15. package/agent-plugin/plugin.json +1 -1
  16. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
  17. package/dist/debugger/android.js +78 -0
  18. package/dist/debugger/guard.js +30 -0
  19. package/dist/debugger/sessions.js +262 -0
  20. package/dist/debugger/worker.js +165 -0
  21. package/dist/interact/android.js +10 -4
  22. package/dist/interact/conditional-action.js +68 -0
  23. package/dist/interact/index.js +70 -14
  24. package/dist/interact/ios.js +14 -8
  25. package/dist/manage/android.js +4 -3
  26. package/dist/manage/ios.js +3 -2
  27. package/dist/observe/android.js +25 -4
  28. package/dist/observe/evidence.js +280 -0
  29. package/dist/observe/index.js +33 -6
  30. package/dist/observe/ios.js +25 -6
  31. package/dist/observe/readiness-service.js +124 -0
  32. package/dist/observe/readiness.js +88 -0
  33. package/dist/observe/recording.js +165 -0
  34. package/dist/observe/snapshot-metadata.js +10 -2
  35. package/dist/observe/system-trace-android.js +135 -0
  36. package/dist/observe/system-trace.js +285 -0
  37. package/dist/server/common.js +9 -0
  38. package/dist/server/condition-schema.js +32 -0
  39. package/dist/server/debugger-handlers.js +38 -0
  40. package/dist/server/debugger-schema.js +42 -0
  41. package/dist/server/journey-schema.js +4 -2
  42. package/dist/server/readiness-schema.js +12 -0
  43. package/dist/server/system-trace-schema.js +17 -0
  44. package/dist/server/tool-definitions.js +51 -0
  45. package/dist/server/tool-handlers.js +294 -105
  46. package/dist/server-core.js +3 -3
  47. package/dist/server.js +34 -0
  48. package/dist/system/index.js +5 -4
  49. package/dist/utils/exec.js +2 -0
  50. package/dist/utils/ios/utils.js +4 -4
  51. package/dist/utils/java.js +14 -12
  52. package/dist/utils/operation-budget.js +26 -2
  53. package/dist/utils/resolve-device.js +9 -0
  54. package/docs/CHANGELOG.md +15 -0
  55. package/docs/specs/020-conditional-actions/intent.md +62 -0
  56. package/docs/specs/020-conditional-actions/plan.md +214 -0
  57. package/docs/specs/020-conditional-actions/spec.md +301 -0
  58. package/docs/specs/021-app-readiness-signals/intent.md +40 -0
  59. package/docs/specs/021-app-readiness-signals/plan.md +102 -0
  60. package/docs/specs/021-app-readiness-signals/spec.md +144 -0
  61. package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
  62. package/docs/specs/022-correlated-evidence/intent.md +37 -0
  63. package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
  64. package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
  65. package/docs/specs/022-correlated-evidence/plan.md +171 -0
  66. package/docs/specs/022-correlated-evidence/spec.md +123 -0
  67. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  68. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  69. package/docs/specs/023-android-system-traces/intent.md +40 -0
  70. package/docs/specs/023-android-system-traces/plan.md +117 -0
  71. package/docs/specs/023-android-system-traces/spec.md +120 -0
  72. package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
  73. package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
  74. package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
  75. package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
  76. package/docs/specs/process.md +213 -0
  77. package/docs/tools/TOOLS.md +5 -0
  78. package/docs/tools/debugger.md +104 -0
  79. package/docs/tools/evidence.md +82 -0
  80. package/docs/tools/interact.md +125 -2
  81. package/docs/tools/observe.md +70 -0
  82. package/docs/tools/system-traces.md +126 -0
  83. package/package.json +1 -1
  84. package/scripts/agent-plugin.mjs +68 -8
  85. package/src/debugger/android.ts +65 -0
  86. package/src/debugger/guard.ts +26 -0
  87. package/src/debugger/sessions.ts +168 -0
  88. package/src/debugger/worker.ts +122 -0
  89. package/src/interact/android.ts +10 -4
  90. package/src/interact/conditional-action.ts +75 -0
  91. package/src/interact/index.ts +57 -28
  92. package/src/interact/ios.ts +14 -8
  93. package/src/manage/android.ts +4 -3
  94. package/src/manage/ios.ts +3 -2
  95. package/src/observe/android.ts +33 -6
  96. package/src/observe/evidence.ts +262 -0
  97. package/src/observe/index.ts +32 -8
  98. package/src/observe/ios.ts +32 -8
  99. package/src/observe/readiness-service.ts +94 -0
  100. package/src/observe/readiness.ts +84 -0
  101. package/src/observe/snapshot-metadata.ts +12 -1
  102. package/src/observe/system-trace-android.ts +133 -0
  103. package/src/observe/system-trace.ts +218 -0
  104. package/src/server/common.ts +7 -0
  105. package/src/server/condition-schema.ts +32 -0
  106. package/src/server/debugger-handlers.ts +29 -0
  107. package/src/server/debugger-schema.ts +37 -0
  108. package/src/server/journey-schema.ts +4 -2
  109. package/src/server/readiness-schema.ts +12 -0
  110. package/src/server/system-trace-schema.ts +17 -0
  111. package/src/server/tool-definitions.ts +49 -0
  112. package/src/server/tool-handlers.ts +182 -21
  113. package/src/server-core.ts +3 -3
  114. package/src/server.ts +24 -0
  115. package/src/system/index.ts +7 -5
  116. package/src/types.ts +30 -1
  117. package/src/utils/exec.ts +2 -0
  118. package/src/utils/ios/utils.ts +4 -4
  119. package/src/utils/java.ts +13 -12
  120. package/src/utils/operation-budget.ts +24 -2
  121. package/src/utils/resolve-device.ts +10 -0
  122. package/test/device/automated/debugger/runtime.android.test.ts +203 -0
  123. package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
  124. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  125. package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
  126. package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
  127. package/test/device/manual/interact/readiness.manual.ts +99 -0
  128. package/test/fixtures/debugger-app/MainActivity.java +46 -0
  129. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  130. package/test/fixtures/debugger-app/README.md +13 -0
  131. package/test/fixtures/debugger-app/build.ts +29 -0
  132. package/test/fixtures/readiness-app/MainActivity.java +23 -0
  133. package/test/fixtures/readiness-app/README.md +12 -0
  134. package/test/fixtures/readiness-app/build.ts +37 -0
  135. package/test/fixtures/readiness-app/main.m +27 -0
  136. package/test/helpers/perfetto.ts +59 -0
  137. package/test/unit/debugger/guards.test.ts +46 -0
  138. package/test/unit/debugger/native.test.ts +33 -0
  139. package/test/unit/debugger/sessions.test.ts +83 -0
  140. package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
  141. package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
  142. package/test/unit/observe/compact_tree_error.test.ts +24 -0
  143. package/test/unit/observe/conditional_snapshots.test.ts +33 -0
  144. package/test/unit/observe/correlated-evidence.test.ts +128 -0
  145. package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
  146. package/test/unit/observe/readiness.test.ts +41 -0
  147. package/test/unit/observe/readiness_native.test.ts +68 -0
  148. package/test/unit/observe/system-trace-android.test.ts +52 -0
  149. package/test/unit/observe/system-trace.test.ts +166 -0
  150. package/test/unit/server/conditional_actions.test.ts +299 -0
  151. package/test/unit/server/conditional_schema.test.ts +59 -0
  152. package/test/unit/server/correlated-evidence.test.ts +194 -0
  153. package/test/unit/server/readiness.test.ts +170 -0
  154. package/test/unit/server/selector_tap_target.test.ts +79 -0
  155. package/test/unit/server/system-trace.test.ts +64 -0
  156. package/test/unit/server/tap_selector.test.ts +2 -0
  157. package/worker/DebuggerWorker.java +160 -0
  158. package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
  159. /package/{skills → .agents/skills}/README.md +0 -0
  160. /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
  161. /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
  162. /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
  163. /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
  164. /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
  165. /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
  166. /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
  167. /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
  168. /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
  169. /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
  170. /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
@@ -1,3 +1,7 @@
1
+ import { checkDebuggerGuard } from '../debugger/guard.js';
2
+ import { currentReadinessWait, observeReadinessWait, isDeviceUnavailable } from '../observe/readiness.js';
3
+ import { readinessDelay } from '../observe/readiness-service.js';
4
+ import { isConditionBlocked } from './conditional-action.js';
1
5
  import { remainingBudget } from '../utils/operation-budget.js';
2
6
  import { createHash } from 'crypto';
3
7
  import { AndroidInteract } from './android.js';
@@ -419,6 +423,8 @@ export class ToolsInteract {
419
423
  }
420
424
  }
421
425
  static async _captureFingerprint(platform, deviceId) {
426
+ if (isConditionBlocked())
427
+ return null;
422
428
  try {
423
429
  const fingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
424
430
  return fingerprint?.fingerprint ?? null;
@@ -1907,6 +1913,7 @@ export class ToolsInteract {
1907
1913
  }
1908
1914
  }
1909
1915
  catch (e) {
1916
+ checkDebuggerGuard();
1910
1917
  console.error('Error scoring element:', e);
1911
1918
  }
1912
1919
  }
@@ -1916,6 +1923,7 @@ export class ToolsInteract {
1916
1923
  }
1917
1924
  }
1918
1925
  catch (e) {
1926
+ checkDebuggerGuard();
1919
1927
  console.error('Error fetching UI tree:', e);
1920
1928
  }
1921
1929
  if (shouldStop || Date.now() > deadline)
@@ -2018,6 +2026,7 @@ export class ToolsInteract {
2018
2026
  }
2019
2027
  }
2020
2028
  catch (e) {
2029
+ checkDebuggerGuard();
2021
2030
  console.error('Error resolving ancestor:', e);
2022
2031
  }
2023
2032
  if (!best)
@@ -2068,7 +2077,8 @@ export class ToolsInteract {
2068
2077
  };
2069
2078
  return { found: true, element: outEl, score: scoreVal, confidence: scoreVal, resolution };
2070
2079
  }
2071
- static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId, singleObservation = false, singleAttempt = false, rejectAmbiguous = false }) {
2080
+ static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId, singleObservation = false, singleAttempt = false, rejectAmbiguous = false, resolveTapTarget = false }) {
2081
+ const readinessTracker = currentReadinessWait();
2072
2082
  const overallStart = Date.now();
2073
2083
  const requestedIndex = typeof match?.index === 'number' ? match.index : null;
2074
2084
  const requested = {
@@ -2127,7 +2137,12 @@ export class ToolsInteract {
2127
2137
  remainingBudget();
2128
2138
  totalPollCount++;
2129
2139
  try {
2140
+ const acquisitionStarted = Date.now();
2130
2141
  const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
2142
+ observeReadinessWait(tree, acquisitionStarted);
2143
+ // A failed acquisition cannot establish that the requested tap target is absent.
2144
+ if (resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error)))
2145
+ throw new Error('ACTION_TIMEOUT');
2131
2146
  const elements = (tree && Array.isArray(tree.elements)) ? tree.elements : [];
2132
2147
  const matches = [];
2133
2148
  for (let i = 0; i < elements.length; i++) {
@@ -2213,7 +2228,8 @@ export class ToolsInteract {
2213
2228
  conditionMet = false;
2214
2229
  }
2215
2230
  else if (condition === 'clickable') {
2216
- matchedElement = chosen ? (ToolsInteract._resolveActionableAncestor(elements, chosen) || chosen) : null;
2231
+ matchedElement = chosen ? (resolveTapTarget && (!ToolsInteract._isVisibleElement(chosen.el) || chosen.el.enabled === false)
2232
+ ? chosen : ToolsInteract._resolveActionableAncestor(elements, chosen) || chosen) : null;
2217
2233
  if (matchedElement) {
2218
2234
  const b = matchedElement.el.bounds;
2219
2235
  const visibleFlag = !!matchedElement.el.visible && Array.isArray(b) && b.length >= 4 && (b[2] > b[0] && b[3] > b[1]);
@@ -2224,6 +2240,13 @@ export class ToolsInteract {
2224
2240
  else
2225
2241
  conditionMet = false;
2226
2242
  }
2243
+ // Waiting for a label to exist/be visible is distinct from resolving its
2244
+ // executable control. Reuse the clickable-wait ancestor policy without
2245
+ // promoting hidden or disabled labels into an unrelated enabled target.
2246
+ if (resolveTapTarget && conditionMet && matchedElement &&
2247
+ ToolsInteract._isVisibleElement(matchedElement.el) && matchedElement.el.enabled !== false) {
2248
+ matchedElement = ToolsInteract._resolveActionableAncestor(elements, matchedElement) ?? matchedElement;
2249
+ }
2227
2250
  const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
2228
2251
  const resolvedDeviceId = tree?.device?.id || deviceId;
2229
2252
  lastMatchedCount = matchedCount;
@@ -2236,7 +2259,7 @@ export class ToolsInteract {
2236
2259
  if (matchedAt === null)
2237
2260
  matchedAt = now;
2238
2261
  stableMatchCount++;
2239
- if (stableMatchCount >= stableObservationCount) {
2262
+ if (stableMatchCount >= stableObservationCount && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
2240
2263
  const latency_ms = now - overallStart;
2241
2264
  const outEl = lastMatchedElement;
2242
2265
  return {
@@ -2261,20 +2284,27 @@ export class ToolsInteract {
2261
2284
  }
2262
2285
  }
2263
2286
  catch (e) {
2287
+ checkDebuggerGuard();
2288
+ // Deadline expiry does not invalidate the last successfully observed state.
2264
2289
  if (e instanceof Error && e.message === 'ACTION_TIMEOUT')
2265
2290
  throw e;
2291
+ if (readinessTracker && !String(e).includes('READINESS_'))
2292
+ readinessTracker.unavailable();
2293
+ if (readinessTracker && isDeviceUnavailable(e))
2294
+ throw e;
2266
2295
  // Non-fatal per-poll error; record and continue
2267
- console.warn('waitForUI: poll error (non-fatal):', e instanceof Error ? e.message : String(e));
2296
+ if (!readinessTracker)
2297
+ console.warn('waitForUI: poll error (non-fatal):', e instanceof Error ? e.message : String(e));
2268
2298
  }
2269
2299
  if (singleAttempt)
2270
2300
  break;
2271
2301
  // Sleep until next poll
2272
- await new Promise(r => setTimeout(r, effectivePoll || 50));
2302
+ await (readinessTracker ? readinessDelay(effectivePoll || 50) : new Promise(r => setTimeout(r, effectivePoll || 50)));
2273
2303
  }
2274
2304
  // Attempt timed out; if more attempts allowed, backoff then retry
2275
2305
  if (attempts < maxAttempts) {
2276
2306
  if (backoff > 0)
2277
- await new Promise(r => setTimeout(r, backoff));
2307
+ await (readinessTracker ? readinessDelay(backoff) : new Promise(r => setTimeout(r, backoff)));
2278
2308
  continue;
2279
2309
  }
2280
2310
  // Final failure for this call
@@ -2290,7 +2320,7 @@ export class ToolsInteract {
2290
2320
  : ` observed ${lastMatchedCount} match(es)`;
2291
2321
  return {
2292
2322
  status: 'timeout',
2293
- error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
2323
+ error: { code: readinessTracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
2294
2324
  metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
2295
2325
  requested,
2296
2326
  observed
@@ -2298,17 +2328,20 @@ export class ToolsInteract {
2298
2328
  }
2299
2329
  }
2300
2330
  catch (err) {
2301
- if (err instanceof Error && err.message === 'ACTION_TIMEOUT')
2331
+ checkDebuggerGuard();
2332
+ if (!readinessTracker && err instanceof Error && err.message === 'ACTION_TIMEOUT')
2333
+ throw err;
2334
+ if (readinessTracker && isDeviceUnavailable(err))
2302
2335
  throw err;
2303
2336
  const elapsed = Date.now() - overallStart;
2304
2337
  return {
2305
2338
  status: 'timeout',
2306
- error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
2339
+ error: { code: readinessTracker && String(err).includes('ACTION_TIMEOUT') ? 'TIMEOUT' : 'INTERNAL_ERROR', message: readinessTracker ? 'Readiness predicate wait did not complete' : err instanceof Error ? err.message : String(err) },
2307
2340
  metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
2308
2341
  requested,
2309
2342
  observed: {
2310
2343
  matched_count: lastMatchedCount,
2311
- condition_satisfied: false,
2344
+ condition_satisfied: readinessTracker ? lastConditionSatisfied : false,
2312
2345
  selected_index: lastMatchedElement?.index ?? null,
2313
2346
  last_matched_element: lastMatchedElement
2314
2347
  }
@@ -2370,12 +2403,14 @@ export class ToolsInteract {
2370
2403
  continue;
2371
2404
  }
2372
2405
  catch (e) {
2406
+ checkDebuggerGuard();
2373
2407
  console.error('Error confirming fingerprint:', e);
2374
2408
  continue;
2375
2409
  }
2376
2410
  }
2377
2411
  }
2378
2412
  catch (e) {
2413
+ checkDebuggerGuard();
2379
2414
  console.error('Error getting screen fingerprint:', e);
2380
2415
  }
2381
2416
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
@@ -2393,6 +2428,7 @@ export class ToolsInteract {
2393
2428
  };
2394
2429
  }
2395
2430
  static async waitForUIChangeHandler({ platform, deviceId, timeout_ms = 60000, stability_window_ms = 300, expected_change, scope = 'screen', target = null }) {
2431
+ const readinessTracker = currentReadinessWait();
2396
2432
  const start = Date.now();
2397
2433
  const pollIntervalMs = 300;
2398
2434
  const stabilityWindow = Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300);
@@ -2415,7 +2451,11 @@ export class ToolsInteract {
2415
2451
  };
2416
2452
  while (Date.now() - start < timeout_ms) {
2417
2453
  try {
2454
+ if (readinessTracker)
2455
+ remainingBudget();
2456
+ const acquisitionStarted = Date.now();
2418
2457
  const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
2458
+ observeReadinessWait(tree, acquisitionStarted);
2419
2459
  const scopedTree = ToolsInteract._resolveUiChangeScope(tree, scope, target);
2420
2460
  if (scopedTree.error) {
2421
2461
  lastScopeResolution = scopedTree.resolution;
@@ -2461,7 +2501,7 @@ export class ToolsInteract {
2461
2501
  candidateSinceMs = Date.now();
2462
2502
  }
2463
2503
  const stableForMs = candidateSinceMs === null ? 0 : Date.now() - candidateSinceMs;
2464
- if (stabilityWindow === 0 || stableForMs >= stabilityWindow) {
2504
+ if ((stabilityWindow === 0 || stableForMs >= stabilityWindow) && (!readinessTracker || readinessTracker.summary.state === 'settled')) {
2465
2505
  return {
2466
2506
  success: true,
2467
2507
  observed_change: candidateObservedChange ?? observedChange,
@@ -2486,10 +2526,19 @@ export class ToolsInteract {
2486
2526
  }
2487
2527
  }
2488
2528
  }
2489
- catch {
2490
- // Keep polling until timeout; the observable surface should be best-effort.
2529
+ catch (error) {
2530
+ checkDebuggerGuard();
2531
+ if (readinessTracker && !String(error).includes('READINESS_'))
2532
+ readinessTracker.unavailable();
2533
+ if (readinessTracker && isDeviceUnavailable(error))
2534
+ throw error;
2535
+ if (readinessTracker && String(error).includes('ACTION_TIMEOUT')) {
2536
+ readinessTracker.deadline();
2537
+ break;
2538
+ }
2539
+ // Default waits retain best-effort polling.
2491
2540
  }
2492
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
2541
+ await (readinessTracker ? readinessDelay(pollIntervalMs) : new Promise(resolve => setTimeout(resolve, pollIntervalMs)));
2493
2542
  }
2494
2543
  return {
2495
2544
  success: false,
@@ -2999,6 +3048,7 @@ export class ToolsInteract {
2999
3048
  }
3000
3049
  }
3001
3050
  catch (err) {
3051
+ checkDebuggerGuard();
3002
3052
  try {
3003
3053
  console.warn('waitForUI: failed to get baseline data (non-fatal):', err instanceof Error ? err.message : String(err));
3004
3054
  }
@@ -3053,10 +3103,12 @@ export class ToolsInteract {
3053
3103
  }
3054
3104
  }
3055
3105
  catch (err) {
3106
+ checkDebuggerGuard();
3056
3107
  console.error('waitForUI(ui) find error:', err);
3057
3108
  }
3058
3109
  }
3059
3110
  catch (err) {
3111
+ checkDebuggerGuard();
3060
3112
  console.error('waitForUI(ui) outer error:', err);
3061
3113
  }
3062
3114
  }
@@ -3090,6 +3142,7 @@ export class ToolsInteract {
3090
3142
  }
3091
3143
  }
3092
3144
  catch (err) {
3145
+ checkDebuggerGuard();
3093
3146
  console.error('waitForUI(log) error:', err);
3094
3147
  }
3095
3148
  }
@@ -3118,6 +3171,7 @@ export class ToolsInteract {
3118
3171
  }
3119
3172
  }
3120
3173
  catch (err) {
3174
+ checkDebuggerGuard();
3121
3175
  console.error('waitForUI(screen) error:', err);
3122
3176
  }
3123
3177
  }
@@ -3142,6 +3196,7 @@ export class ToolsInteract {
3142
3196
  }
3143
3197
  }
3144
3198
  catch (err) {
3199
+ checkDebuggerGuard();
3145
3200
  console.error('waitForUI(idle) error:', err);
3146
3201
  }
3147
3202
  }
@@ -3159,6 +3214,7 @@ export class ToolsInteract {
3159
3214
  snapshot = await Obs.ToolsObserve.captureDebugSnapshotHandler({ reason: `wait_for_ui timeout for ${type}`, includeLogs: true, platform, deviceId });
3160
3215
  }
3161
3216
  catch (err) {
3217
+ checkDebuggerGuard();
3162
3218
  snapshot = { error: err instanceof Error ? err.message : String(err) };
3163
3219
  }
3164
3220
  }
@@ -1,3 +1,5 @@
1
+ import { evidenceDispatch } from '../observe/evidence.js';
2
+ import { beforeInputDispatch } from './conditional-action.js';
1
3
  import { remainingBudget, markDispatchStarted } from '../utils/operation-budget.js';
2
4
  import { spawn } from "child_process";
3
5
  import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../utils/ios/utils.js";
@@ -24,9 +26,11 @@ export class iOSInteract {
24
26
  if (targetUdid) {
25
27
  args.push('--udid', targetUdid);
26
28
  }
27
- await new Promise((resolve, reject) => {
28
- markDispatchStarted();
29
- const proc = spawn(getIdbCmd(), args, { timeout: remainingBudget() || undefined });
29
+ await beforeInputDispatch('ios', device.id);
30
+ const timeout = remainingBudget() || undefined;
31
+ markDispatchStarted();
32
+ await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise((resolve, reject) => {
33
+ const proc = spawn(getIdbCmd(), args, { timeout });
30
34
  let stderr = '';
31
35
  proc.stderr.on('data', d => stderr += d.toString());
32
36
  proc.on('close', code => {
@@ -36,7 +40,7 @@ export class iOSInteract {
36
40
  reject(new Error(`idb ui tap failed: ${stderr}`));
37
41
  });
38
42
  proc.on('error', err => reject(err));
39
- });
43
+ }));
40
44
  return { device, success: true, x, y };
41
45
  }
42
46
  catch (e) {
@@ -64,9 +68,11 @@ export class iOSInteract {
64
68
  if (targetUdid) {
65
69
  args.push('--udid', targetUdid);
66
70
  }
67
- await new Promise((resolve, reject) => {
68
- markDispatchStarted();
69
- const proc = spawn(getIdbCmd(), args, { timeout: remainingBudget() || undefined });
71
+ await beforeInputDispatch('ios', device.id);
72
+ const timeout = remainingBudget() || undefined;
73
+ markDispatchStarted();
74
+ await evidenceDispatch({ platform: 'ios', id: device.id }, () => new Promise((resolve, reject) => {
75
+ const proc = spawn(getIdbCmd(), args, { timeout });
70
76
  let stderr = '';
71
77
  proc.stderr.on('data', d => stderr += d.toString());
72
78
  proc.on('close', code => {
@@ -76,7 +82,7 @@ export class iOSInteract {
76
82
  reject(new Error(`idb ui swipe failed: ${stderr}`));
77
83
  });
78
84
  proc.on('error', err => reject(err));
79
- });
85
+ }));
80
86
  return { device, success: true, start: [x1, y1], end: [x2, y2], duration };
81
87
  }
82
88
  catch (e) {
@@ -1,3 +1,4 @@
1
+ import { evidenceDispatch } from '../observe/evidence.js';
1
2
  import { hasFiniteBudget, markDispatchStarted, remainingBudget, runWithBudget } from '../utils/operation-budget.js';
2
3
  import { promises as fs } from 'fs';
3
4
  import { spawn } from 'child_process';
@@ -152,7 +153,7 @@ export class AndroidManage {
152
153
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
153
154
  try {
154
155
  markDispatchStarted();
155
- const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs });
156
+ const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId, { timeout: timeoutMs }));
156
157
  const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null);
157
158
  return {
158
159
  device: deviceInfo,
@@ -178,7 +179,7 @@ export class AndroidManage {
178
179
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
179
180
  try {
180
181
  markDispatchStarted();
181
- await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
182
+ await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'am', 'force-stop', appId], deviceId));
182
183
  return { device: deviceInfo, appTerminated: true };
183
184
  }
184
185
  catch (e) {
@@ -211,7 +212,7 @@ export class AndroidManage {
211
212
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
212
213
  try {
213
214
  markDispatchStarted();
214
- const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
215
+ const output = await evidenceDispatch({ platform: 'android', id: deviceInfo.id }, () => execAdb(['shell', 'pm', 'clear', appId], deviceId));
215
216
  return { device: deviceInfo, dataCleared: output === 'Success' };
216
217
  }
217
218
  catch (e) {
@@ -1,3 +1,4 @@
1
+ import { evidenceDispatch } from '../observe/evidence.js';
1
2
  import { hasFiniteBudget, markDispatchStarted, remainingBudget, runWithBudget } from '../utils/operation-budget.js';
2
3
  import { promises as fs } from "fs";
3
4
  import { spawn, spawnSync } from "child_process";
@@ -293,7 +294,7 @@ export class iOSManage {
293
294
  validateBundleId(bundleId);
294
295
  try {
295
296
  markDispatchStarted();
296
- const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
297
+ const result = await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'launch', deviceId, bundleId], deviceId));
297
298
  const device = await getIOSDeviceMetadata(deviceId);
298
299
  const fingerprint = observe ? await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null) : null;
299
300
  const pidMatch = result.output.match(/:\s*(\d+)\s*$/);
@@ -320,7 +321,7 @@ export class iOSManage {
320
321
  validateBundleId(bundleId);
321
322
  try {
322
323
  markDispatchStarted();
323
- await execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId);
324
+ await evidenceDispatch({ platform: 'ios', id: deviceId }, () => execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId));
324
325
  const device = await getIOSDeviceMetadata(deviceId);
325
326
  return { device, appTerminated: true };
326
327
  }
@@ -1,3 +1,4 @@
1
+ import { captureEvidence } from './evidence.js';
1
2
  import { spawn } from "child_process";
2
3
  import { XMLParser } from "fast-xml-parser";
3
4
  import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "../utils/android/utils.js";
@@ -11,10 +12,14 @@ import { remainingBudget } from '../utils/operation-budget.js';
11
12
  import { createObservationMeasurement } from './performance-metrics.js';
12
13
  const activeLogStreams = new Map();
13
14
  export class AndroidObserve {
15
+ getLogStreamDevice(sessionId = 'default') { return activeLogStreams.get(sessionId)?.device ?? null; }
14
16
  async getDeviceMetadata(appId, deviceId) {
15
17
  return getAndroidDeviceMetadata(appId, deviceId);
16
18
  }
17
19
  async getUITree(deviceId) {
20
+ return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId));
21
+ }
22
+ async collectGetUITree(deviceId) {
18
23
  const measurement = createObservationMeasurement('android');
19
24
  let metadata;
20
25
  try {
@@ -113,6 +118,9 @@ export class AndroidObserve {
113
118
  }
114
119
  }
115
120
  async getLogs(filters = {}) {
121
+ return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters));
122
+ }
123
+ async collectGetLogs(filters = {}) {
116
124
  const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters;
117
125
  const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
118
126
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
@@ -213,6 +221,9 @@ export class AndroidObserve {
213
221
  }
214
222
  }
215
223
  async captureScreen(deviceId) {
224
+ return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId));
225
+ }
226
+ async collectCaptureScreen(deviceId) {
216
227
  const metadata = await getAndroidDeviceMetadata("", deviceId);
217
228
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
218
229
  return new Promise((resolve, reject) => {
@@ -331,6 +342,9 @@ export class AndroidObserve {
331
342
  });
332
343
  }
333
344
  async getCurrentScreen(deviceId) {
345
+ return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId));
346
+ }
347
+ async collectGetCurrentScreen(deviceId) {
334
348
  const metadata = await getAndroidDeviceMetadata("", deviceId);
335
349
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
336
350
  try {
@@ -385,12 +399,15 @@ export class AndroidObserve {
385
399
  }
386
400
  }
387
401
  async getScreenFingerprint(deviceId) {
402
+ return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId));
403
+ }
404
+ async collectGetScreenFingerprint(deviceId) {
388
405
  try {
389
406
  const tree = await this.getUITree(deviceId);
390
407
  if (!tree || tree.error)
391
408
  return { fingerprint: null, error: tree.error };
392
409
  const current = await this.getCurrentScreen(deviceId).catch(() => null);
393
- return computeScreenFingerprint(tree, current, 'android', 50);
410
+ return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms };
394
411
  }
395
412
  catch (e) {
396
413
  return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
@@ -411,7 +428,7 @@ export class AndroidObserve {
411
428
  catch { }
412
429
  activeLogStreams.delete(sessionId);
413
430
  }
414
- const args = ['logcat', `--pid=${pid}`, filter];
431
+ const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter];
415
432
  const proc = spawn(getAdbCmd(), args);
416
433
  const tmpDir = process.env.TMPDIR || '/tmp';
417
434
  const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
@@ -434,9 +451,10 @@ export class AndroidObserve {
434
451
  });
435
452
  proc.on('close', () => {
436
453
  stream.end();
437
- activeLogStreams.delete(sessionId);
454
+ if (activeLogStreams.get(sessionId)?.proc === proc)
455
+ activeLogStreams.delete(sessionId);
438
456
  });
439
- activeLogStreams.set(sessionId, { proc, file });
457
+ activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } });
440
458
  return { success: true, stream_started: true };
441
459
  }
442
460
  catch (e) {
@@ -455,6 +473,9 @@ export class AndroidObserve {
455
473
  return { success: true };
456
474
  }
457
475
  async readLogStream(sessionId = 'default', limit = 100, since) {
476
+ return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since));
477
+ }
478
+ async collectReadLogStream(sessionId = 'default', limit = 100, since) {
458
479
  const entry = activeLogStreams.get(sessionId);
459
480
  let file;
460
481
  if (entry && entry.file)