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,7 +1,18 @@
1
+ import { handleDebugger } from './debugger-handlers.js';
2
+ import { debuggerContracts } from './debugger-schema.js';
3
+ import { debuggerSessions } from '../debugger/sessions.js';
4
+ import { withDebuggerGuard, guardDispatched } from '../debugger/guard.js';
5
+ import { systemTraces, TraceError } from '../observe/system-trace.js';
6
+ import { systemTraceTools, traceTargetSchema, traceStartSchema, traceControlSchema } from './system-trace-schema.js';
7
+ import { currentEvidenceCorrelationId, bindSystemTrace, withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js';
1
8
  import { measure, withTiming } from '../utils/timing.js';
2
9
  import { randomUUID } from 'node:crypto';
3
10
  import { runWithBudget } from '../utils/operation-budget.js';
4
11
  import { journeySchema, tapElementControlsSchema } from './journey-schema.js';
12
+ import { readinessSchema, readinessWaitSchema } from './readiness-schema.js';
13
+ import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js';
14
+ import { validateExpectedState } from './condition-schema.js';
15
+ import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js';
5
16
  import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js';
6
17
  import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js';
7
18
  import { ToolsInteract } from '../interact/index.js';
@@ -11,6 +22,7 @@ import { ToolsNetwork } from '../network/index.js';
11
22
  import { getSystemStatus } from '../system/index.js';
12
23
  import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js';
13
24
  import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
25
+ const systemTraceToolNames = new Set(systemTraceTools.map(tool => tool.name));
14
26
  function interactionControls(args) {
15
27
  const verificationMode = getStringArg(args, 'verificationMode') ?? 'full';
16
28
  const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
@@ -43,18 +55,45 @@ function compactAction(result, verificationMode, started = true, stateDelta, tot
43
55
  timing: { total_ms: totalMs }
44
56
  };
45
57
  }
46
- function actionHandler(type, execute) {
58
+ function correlatedHandler(name, handler, action = false, journey = false) {
47
59
  return async (args) => {
60
+ const ref = validateActionRef(args.actionRef);
61
+ const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
62
+ try {
63
+ return await handler(args);
64
+ }
65
+ catch (error) {
66
+ return error instanceof TraceError ? { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true } : wrapToolError(name, error);
67
+ }
68
+ });
69
+ const item = result.content.find(item => item.type === 'text');
70
+ if (item?.text) {
71
+ const payload = JSON.parse(item.text);
72
+ payload.evidence = evidence;
73
+ item.text = JSON.stringify(payload);
74
+ }
75
+ return result;
76
+ };
77
+ }
78
+ function actionHandler(type, execute) {
79
+ return correlatedHandler(type, async (args) => {
48
80
  const timing = {};
49
81
  return withTiming(timing, async () => {
50
82
  const started = performance.now();
51
83
  const controls = interactionControls(args);
84
+ const expected = validateExpectedState(type, args);
85
+ const condition = expected ? createConditionalAction(args.platform, args.deviceId, expected) : undefined;
52
86
  const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000);
53
87
  let dispatchStarted = false;
54
88
  let result;
55
89
  const dispatchWithBudget = (timeoutMs) => runWithBudget(timeoutMs, async (budget) => {
56
90
  try {
57
- return payloadOf(await execute(args));
91
+ if (!condition)
92
+ return payloadOf(await execute(args));
93
+ return await withConditionalAction(condition, async () => {
94
+ const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId });
95
+ return withResolvedDevice(device, async () => payloadOf(await execute(args)));
96
+ });
58
97
  }
59
98
  finally {
60
99
  dispatchStarted = budget.dispatchStarted;
@@ -68,17 +107,28 @@ function actionHandler(type, execute) {
68
107
  dispatchStarted = true;
69
108
  }
70
109
  catch (error) {
71
- if (controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
110
+ if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error)))
72
111
  return wrapToolError(type, error);
73
112
  result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) };
74
113
  }
114
+ const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status);
115
+ if (blocked) {
116
+ dispatchStarted = false;
117
+ result = buildActionExecutionResult({
118
+ actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
119
+ selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
120
+ success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
121
+ failure: { failureCode: conditionFailureCode(condition), retryable: false },
122
+ traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
123
+ });
124
+ }
75
125
  if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? ''))
76
126
  result.failure_code = 'ACTION_TIMEOUT';
77
127
  if (result.failure_code === 'ACTION_TIMEOUT')
78
128
  result.retryable = !dispatchStarted;
79
129
  if (!result.success && typeof result.retryable !== 'boolean')
80
130
  result.retryable = !dispatchStarted;
81
- result.action_id ??= randomUUID();
131
+ result.action_id ??= currentEvidenceActionId() ?? randomUUID();
82
132
  result.action_type ??= type;
83
133
  recordObservationEvent({
84
134
  step_id: args._stepId,
@@ -92,6 +142,7 @@ function actionHandler(type, execute) {
92
142
  if (result.success && controls.verificationMode === 'light') {
93
143
  const verificationStarted = performance.now();
94
144
  const tree = await withObservationContext({ stepId: args._stepId, purpose: 'verification' }, () => measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id })))).catch(() => null);
145
+ markEvidenceVerification();
95
146
  observationAvailable = !!tree && !tree.error;
96
147
  recordObservationEvent({
97
148
  verification_budget_ms: controls.verificationTimeoutMs,
@@ -102,11 +153,21 @@ function actionHandler(type, execute) {
102
153
  if (observationAvailable && tree.device)
103
154
  delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
104
155
  }
156
+ if (!blocked && controls.verificationMode === 'full' && dispatchStarted)
157
+ markEvidenceVerification();
105
158
  const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started);
159
+ if (blocked)
160
+ compact.verification.status = 'not_requested';
106
161
  if (result.success && controls.verificationMode === 'light' && !observationAvailable)
107
162
  compact.verification.status = 'unavailable';
108
163
  const evidence = {
109
164
  ...compact,
165
+ ...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
166
+ ? { ...condition.result, observed: condition.result.observed ? {
167
+ snapshot_session_id: condition.result.observed.snapshot_session_id,
168
+ snapshot_revision: condition.result.observed.snapshot_revision,
169
+ captured_at_ms: condition.result.observed.captured_at_ms
170
+ } : null } : condition.result } : {}),
110
171
  ...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
111
172
  ...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
112
173
  };
@@ -115,7 +176,7 @@ function actionHandler(type, execute) {
115
176
  effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
116
177
  }, controls.responseMode === 'debug');
117
178
  });
118
- };
179
+ }, true);
119
180
  }
120
181
  const handleStartApp = actionHandler('start_app', executeStartApp);
121
182
  const handleRestartApp = actionHandler('restart_app', executeRestartApp);
@@ -131,7 +192,7 @@ async function executeStartApp(args) {
131
192
  const deviceId = getStringArg(args, 'deviceId');
132
193
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
133
194
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
134
- ToolsNetwork.notifyActionStart();
195
+ notifyActionStart();
135
196
  const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')));
136
197
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
137
198
  const result = buildActionExecutionResult({
@@ -168,7 +229,7 @@ async function executeRestartApp(args) {
168
229
  const deviceId = getStringArg(args, 'deviceId');
169
230
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
170
231
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
171
- ToolsNetwork.notifyActionStart();
232
+ notifyActionStart();
172
233
  const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined;
173
234
  const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)));
174
235
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
@@ -381,9 +442,12 @@ async function handleAdjustControl(args) {
381
442
  platform,
382
443
  deviceId
383
444
  });
445
+ markEvidenceVerification();
384
446
  return wrapResponse(res);
385
447
  }
386
448
  async function handleWaitForUI(args) {
449
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
450
+ throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
387
451
  const selector = getObjectArg(args, 'selector');
388
452
  const condition = getStringArg(args, 'condition') ?? 'exists';
389
453
  const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
@@ -392,10 +456,13 @@ async function handleWaitForUI(args) {
392
456
  const retry = getObjectArg(args, 'retry');
393
457
  const platform = getStringArg(args, 'platform');
394
458
  const deviceId = getStringArg(args, 'deviceId');
395
- const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId });
459
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId });
460
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute();
396
461
  return wrapResponse(res);
397
462
  }
398
463
  async function handleWaitForUIChange(args) {
464
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean')
465
+ throw new Error('INVALID_ARGUMENT: requireReady must be boolean');
399
466
  const platform = getStringArg(args, 'platform');
400
467
  const deviceId = getStringArg(args, 'deviceId');
401
468
  const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
@@ -403,7 +470,8 @@ async function handleWaitForUIChange(args) {
403
470
  const expected_change = getStringArg(args, 'expected_change');
404
471
  const scope = getStringArg(args, 'scope');
405
472
  const target = getStringArg(args, 'target');
406
- const res = await ToolsInteract.waitForUIChangeHandler({ platform, deviceId, timeout_ms, stability_window_ms, expected_change, scope, target });
473
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target });
474
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms, device => execute(device.platform, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute();
407
475
  return wrapResponse(res);
408
476
  }
409
477
  async function handleFindElement(args) {
@@ -424,7 +492,7 @@ async function executeTap(args) {
424
492
  const deviceId = getStringArg(args, 'deviceId');
425
493
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
426
494
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
427
- ToolsNetwork.notifyActionStart();
495
+ notifyActionStart();
428
496
  const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }));
429
497
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
430
498
  const result = buildActionExecutionResult({
@@ -450,14 +518,15 @@ async function executeTapElement(args) {
450
518
  if (selector) {
451
519
  const waitFor = getObjectArg(args, 'waitFor');
452
520
  const targetStarted = performance.now();
453
- const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(waitFor?.timeoutMs ?? 1500, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: waitFor?.timeoutMs ?? 1500, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
521
+ const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000;
522
+ const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
454
523
  recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
455
524
  if (resolution.status !== 'success' || !resolution.element?.elementId)
456
525
  return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
457
526
  resolvedId = resolution.element.elementId;
458
527
  freshTree = resolution._tree;
459
528
  }
460
- ToolsNetwork.notifyActionStart();
529
+ notifyActionStart();
461
530
  if (freshTree)
462
531
  recordObservationConsumer(freshTree, 'target', args._stepId);
463
532
  const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
@@ -508,102 +577,125 @@ async function handleRunJourney(args) {
508
577
  const steps = parsed.data.steps;
509
578
  const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId });
510
579
  const deviceId = device.id;
511
- return withResolvedDevice(device, async () => {
512
- const started = performance.now();
513
- const results = [];
514
- let stoppedAt = null;
515
- let pendingLightStep = null;
516
- for (let index = 0; index < steps.length; index++) {
517
- const step = steps[index];
518
- const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full';
519
- const next = steps[index + 1];
520
- const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
521
- next.assertion?.kind === 'element_visible';
522
- const stepStart = performance.now();
523
- let result;
524
- if (stoppedAt) {
525
- results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
526
- continue;
527
- }
528
- try {
529
- if (step.type === 'tap') {
530
- result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
580
+ const finishTrace = parsed.data.systemTraceId
581
+ ? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()) : undefined;
582
+ if (parsed.data.systemTraceId)
583
+ bindSystemTrace(parsed.data.systemTraceId);
584
+ try {
585
+ return await withResolvedDevice(device, async () => {
586
+ const started = performance.now();
587
+ const results = [];
588
+ let stoppedAt = null;
589
+ let pendingLightStep = null;
590
+ for (let index = 0; index < steps.length; index++) {
591
+ const step = steps[index];
592
+ if (platform === 'android' && debuggerSessions.paused(deviceId)) {
593
+ results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } });
594
+ stoppedAt = step.id;
595
+ break;
531
596
  }
532
- else if (step.type === 'wait') {
533
- result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
534
- result = { success: result.status === 'success', ...result };
597
+ const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full';
598
+ const next = steps[index + 1];
599
+ const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
600
+ next.assertion?.kind === 'element_visible';
601
+ const stepStart = performance.now();
602
+ let result;
603
+ if (stoppedAt) {
604
+ results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
605
+ continue;
535
606
  }
536
- else if (step.type === 'assert') {
537
- result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
538
- const assertion = step.assertion;
539
- if (assertion?.kind === 'element_visible')
540
- result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
541
- else if (assertion?.kind === 'element_absent')
542
- result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: step.timeoutMs ?? 5000, pollIntervalMs: step.pollIntervalMs ?? 100, platform, deviceId });
543
- else if (assertion?.kind === 'screen_fingerprint')
544
- result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: step.timeoutMs ?? 5000, platform, deviceId });
545
- else if (assertion?.kind === 'state_equals')
546
- result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true });
547
- else
548
- result = { success: false, failure_code: 'INVALID_ASSERTION' };
549
- return result;
550
- }));
607
+ try {
608
+ if (step.type === 'tap') {
609
+ result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
610
+ }
611
+ else if (step.type === 'wait') {
612
+ result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
613
+ result = { success: result.status === 'success', ...result };
614
+ }
615
+ else if (step.type === 'assert') {
616
+ result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
617
+ const assertion = step.assertion;
618
+ if (assertion?.kind === 'element_visible')
619
+ result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
620
+ else if (assertion?.kind === 'element_absent')
621
+ result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: step.timeoutMs ?? 5000, pollIntervalMs: step.pollIntervalMs ?? 100, platform, deviceId });
622
+ else if (assertion?.kind === 'screen_fingerprint')
623
+ result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: step.timeoutMs ?? 5000, platform, deviceId });
624
+ else if (assertion?.kind === 'state_equals')
625
+ result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true });
626
+ else
627
+ result = { success: false, failure_code: 'INVALID_ASSERTION' };
628
+ return result;
629
+ }));
630
+ }
631
+ else if (step.type === 'start_app') {
632
+ result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
633
+ }
634
+ else {
635
+ result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
636
+ }
551
637
  }
552
- else if (step.type === 'start_app') {
553
- result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
638
+ catch (error) {
639
+ result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
554
640
  }
555
- else {
556
- result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
641
+ if (platform === 'android' && debuggerSessions.paused(deviceId)) {
642
+ result.success = false;
643
+ result.failure_code = 'DEBUGGER_PAUSED';
644
+ result.retryable = false;
557
645
  }
558
- }
559
- catch (error) {
560
- result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
561
- }
562
- if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? ''))
563
- result.failure_code = 'ASSERTION_TIMEOUT';
564
- const success = !!result?.success;
565
- if (lightCoveredByAssertion) {
566
- result.verification = { mode: 'light', status: success ? 'unavailable' : 'failed' };
567
- if (success) {
568
- result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
569
- pendingLightStep = results.length;
646
+ if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? ''))
647
+ result.failure_code = 'ASSERTION_TIMEOUT';
648
+ const success = !!result?.success;
649
+ if (lightCoveredByAssertion) {
650
+ result.verification = { mode: 'light', status: success ? 'unavailable' :
651
+ (['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') };
652
+ if (success) {
653
+ result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true };
654
+ pendingLightStep = results.length;
655
+ }
570
656
  }
571
- }
572
- if (step.type === 'assert' && pendingLightStep !== null) {
573
- const pending = results[pendingLightStep].result;
574
- if (typeof result?.observed?.matched_count === 'number') {
575
- pending.verification.status = 'observed';
576
- delete pending.verification_diagnostic;
577
- const delta = ToolsObserve.getLatestStateDelta(platform, deviceId);
578
- if (delta) {
579
- pending.state_delta = delta;
580
- pending.state_delta_available = true;
657
+ if (step.type === 'assert' && pendingLightStep !== null) {
658
+ const pending = results[pendingLightStep].result;
659
+ if (typeof result?.observed?.matched_count === 'number') {
660
+ pending.verification.status = 'observed';
661
+ if (pending.evidence)
662
+ completeJourneyVerification(pending.evidence.action_id);
663
+ delete pending.verification_diagnostic;
664
+ const delta = ToolsObserve.getLatestStateDelta(platform, deviceId);
665
+ if (delta) {
666
+ pending.state_delta = delta;
667
+ pending.state_delta_available = true;
668
+ }
581
669
  }
670
+ pendingLightStep = null;
582
671
  }
583
- pendingLightStep = null;
672
+ if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
673
+ result = {
674
+ success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
675
+ ...(result.error?.code ? { failure_code: result.error.code } : {}),
676
+ ...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
677
+ ...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
678
+ ...(result.actual_state ? { observed: result.actual_state } : {})
679
+ };
680
+ results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } });
681
+ if (!success)
682
+ stoppedAt = step.id;
584
683
  }
585
- if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
586
- result = {
587
- success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
588
- ...(result.error?.code ? { failure_code: result.error.code } : {}),
589
- ...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
590
- ...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
591
- ...(result.actual_state ? { observed: result.actual_state } : {})
592
- };
593
- results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } });
594
- if (!success)
595
- stoppedAt = step.id;
596
- }
597
- const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
598
- ? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined;
599
- const componentTiming = {};
600
- for (const step of results)
601
- for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
602
- if (key !== 'total_ms' && typeof value === 'number')
603
- componentTiming[key] = (componentTiming[key] ?? 0) + value;
604
- }
605
- return wrapResponse({ success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug');
606
- });
684
+ const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
685
+ ? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined;
686
+ const componentTiming = {};
687
+ for (const step of results)
688
+ for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
689
+ if (key !== 'total_ms' && typeof value === 'number')
690
+ componentTiming[key] = (componentTiming[key] ?? 0) + value;
691
+ }
692
+ refreshJourneyEvidence(results);
693
+ return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug');
694
+ });
695
+ }
696
+ finally {
697
+ await finishTrace?.();
698
+ }
607
699
  }
608
700
  async function executeSwipe(args) {
609
701
  const platform = getStringArg(args, 'platform') ?? 'android';
@@ -615,7 +707,7 @@ async function executeSwipe(args) {
615
707
  const deviceId = getStringArg(args, 'deviceId');
616
708
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
617
709
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
618
- ToolsNetwork.notifyActionStart();
710
+ notifyActionStart();
619
711
  const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }));
620
712
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
621
713
  const result = buildActionExecutionResult({
@@ -638,7 +730,7 @@ async function executeScrollToElement(args) {
638
730
  const deviceId = getStringArg(args, 'deviceId');
639
731
  const { verificationMode } = interactionControls(args);
640
732
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
641
- ToolsNetwork.notifyActionStart();
733
+ notifyActionStart();
642
734
  const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }));
643
735
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null;
644
736
  const result = buildActionExecutionResult({
@@ -666,7 +758,7 @@ async function executeTypeText(args) {
666
758
  const deviceId = getStringArg(args, 'deviceId');
667
759
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
668
760
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
669
- ToolsNetwork.notifyActionStart();
761
+ notifyActionStart();
670
762
  const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }));
671
763
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
672
764
  const result = buildActionExecutionResult({
@@ -684,7 +776,7 @@ async function executePressBack(args) {
684
776
  const deviceId = getStringArg(args, 'deviceId');
685
777
  const { verificationMode, actionTimeoutMs } = interactionControls(args);
686
778
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
687
- ToolsNetwork.notifyActionStart();
779
+ notifyActionStart();
688
780
  const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }));
689
781
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null;
690
782
  const result = buildActionExecutionResult({
@@ -742,7 +834,50 @@ async function handleGetNetworkActivity(args) {
742
834
  const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId });
743
835
  return wrapResponse(result);
744
836
  }
837
+ async function handleSystemTrace(name, args) {
838
+ const schema = name === 'start_system_trace' ? traceStartSchema : name === 'get_system_trace_capabilities' ? traceTargetSchema : traceControlSchema;
839
+ const parsed = schema.safeParse(args);
840
+ if (!parsed.success)
841
+ throw new TraceError('INVALID_ARGUMENT');
842
+ if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
843
+ const target = parsed.data;
844
+ if (target.platform !== 'android') {
845
+ if (name === 'start_system_trace')
846
+ throw new TraceError('UNSUPPORTED_PLATFORM');
847
+ return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: {}, reasons: ['UNSUPPORTED_PLATFORM'], enabled_sources: [], unavailable_categories: [], limitations: ['Android only'] });
848
+ }
849
+ let device;
850
+ try {
851
+ device = await resolveTargetDevice(target);
852
+ }
853
+ catch {
854
+ if (name === 'start_system_trace')
855
+ throw new TraceError('TRACE_UNSUPPORTED');
856
+ return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: { device: false }, reasons: ['DEVICE_UNAVAILABLE'], enabled_sources: [], unavailable_categories: [], limitations: ['An authorised Android device is required'] });
857
+ }
858
+ if (name === 'get_system_trace_capabilities')
859
+ return wrapResponse(await systemTraces.capabilities(device.id));
860
+ const result = await systemTraces.start(device.id, target.durationMs);
861
+ return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) };
862
+ }
863
+ const operation = name === 'stop_system_trace' ? 'stop' : name === 'cancel_system_trace' ? 'cancel' : 'status';
864
+ const result = await systemTraces.control(parsed.data.traceId, operation);
865
+ return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) };
866
+ }
745
867
  export const toolHandlers = {
868
+ ...Object.fromEntries(systemTraceTools.map(({ name }) => [name, (args) => handleSystemTrace(name, args)])),
869
+ get_app_readiness: async (args) => {
870
+ const parsed = readinessSchema.safeParse(args);
871
+ if (!parsed.success)
872
+ throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
873
+ return wrapResponse(await getAppReadiness(parsed.data));
874
+ },
875
+ wait_for_app_ready: async (args) => {
876
+ const parsed = readinessWaitSchema.safeParse(args);
877
+ if (!parsed.success)
878
+ throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
879
+ return wrapResponse(await getAppReadiness(parsed.data, true));
880
+ },
746
881
  start_app: handleStartApp,
747
882
  terminate_app: handleTerminateApp,
748
883
  restart_app: handleRestartApp,
@@ -779,7 +914,12 @@ export const toolHandlers = {
779
914
  classify_action_outcome: handleClassifyActionOutcome,
780
915
  get_network_activity: handleGetNetworkActivity
781
916
  };
782
- export async function handleToolCall(name, args = {}) {
917
+ for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
918
+ toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey');
919
+ }
920
+ export async function handleToolCall(name, args = {}, signal) {
921
+ if (Object.hasOwn(debuggerContracts, name))
922
+ return handleDebugger(name, args, signal);
783
923
  const handler = toolHandlers[name];
784
924
  if (!handler)
785
925
  throw new Error(`Unknown tool: ${name}`);
@@ -801,9 +941,58 @@ export async function handleToolCall(name, args = {}) {
801
941
  };
802
942
  return withTiming(measured, async () => {
803
943
  try {
804
- return addTiming(await handler(args));
944
+ if (!systemTraceToolNames.has(name))
945
+ validateExpectedState(name, args);
946
+ const guarded = ['start_app', 'terminate_app', 'restart_app', 'reset_app_data', 'install_app', 'build_and_install', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'run_journey', 'wait_for_ui', 'wait_for_ui_change', 'wait_for_screen_change', 'expect_screen', 'expect_element_visible', 'expect_state'].includes(name);
947
+ if (guarded && args.platform !== 'ios' && debuggerSessions.hasActiveSessions()) {
948
+ const device = await resolveTargetDevice({ platform: 'android', deviceId: args.deviceId });
949
+ return withResolvedDevice(device, () => withDebuggerGuard('android', device.id, async () => {
950
+ const before = debuggerSessions.paused(device.id);
951
+ if (before)
952
+ return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before });
953
+ let response;
954
+ try {
955
+ response = await handler(args);
956
+ }
957
+ catch (error) {
958
+ if (!debuggerSessions.paused(device.id))
959
+ throw error;
960
+ response = wrapResponse({ success: false });
961
+ }
962
+ const pause = debuggerSessions.paused(device.id);
963
+ if (pause) {
964
+ const item = response.content.find(item => item.type === 'text');
965
+ if (item?.text) {
966
+ const payload = JSON.parse(item.text);
967
+ Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
968
+ dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
969
+ verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause });
970
+ item.text = JSON.stringify(payload);
971
+ }
972
+ }
973
+ return addTiming(response);
974
+ }));
975
+ }
976
+ const response = await handler(args);
977
+ if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name) && args.platform !== 'ios') {
978
+ const item = response.content.find(item => item.type === 'text');
979
+ if (item?.text) {
980
+ try {
981
+ const payload = JSON.parse(item.text);
982
+ const device = payload.device ?? payload.evidence?.device;
983
+ const id = device?.id ?? args.deviceId;
984
+ const pause = id && device?.platform !== 'ios' ? debuggerSessions.paused(String(id)) : null;
985
+ if (pause)
986
+ item.text = JSON.stringify({ ...payload, debugger: pause });
987
+ }
988
+ catch { /* Non-JSON observation */ }
989
+ }
990
+ }
991
+ return addTiming(response);
805
992
  }
806
993
  catch (error) {
994
+ if (error instanceof TraceError)
995
+ return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true };
807
996
  console.error(`Error executing tool ${name}:`, error);
808
997
  return addTiming(wrapToolError(name, error));
809
998
  }