mobile-debug-mcp 0.27.0 → 0.29.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.
- package/dist/interact/index.js +238 -11
- package/dist/server/common.js +134 -3
- package/dist/server-core.js +12 -2
- package/dist/server.js +5 -3
- package/docs/CHANGELOG.md +7 -0
- package/docs/ROADMAP.md +19 -22
- package/docs/rfcs/{012.md → 012-action-trace-and-xecution-observability.md} +43 -4
- package/docs/specs/mcp-tooling-spec-v1.md +14 -0
- package/docs/tools/interact.md +44 -0
- package/package.json +1 -1
- package/src/interact/index.ts +268 -12
- package/src/server/common.ts +194 -4
- package/src/server-core.ts +17 -1
- package/src/server.ts +5 -3
- package/src/types.ts +23 -0
- package/test/device/manual/observe/rfc012_trace.manual.ts +51 -0
- package/test/unit/interact/expect_tools.test.ts +57 -25
- package/test/unit/server/common.test.ts +24 -0
package/src/interact/index.ts
CHANGED
|
@@ -6,8 +6,9 @@ export { AndroidInteract, iOSInteract };
|
|
|
6
6
|
import { resolveTargetDevice } from '../utils/resolve-device.js'
|
|
7
7
|
import { ToolsObserve } from '../observe/index.js'
|
|
8
8
|
import { computeSnapshotSignature } from '../observe/snapshot-metadata.js'
|
|
9
|
-
import { buildActionExecutionResult } from '../server/common.js'
|
|
9
|
+
import { buildActionExecutionResult, createTraceStep, nextActionId } from '../server/common.js'
|
|
10
10
|
import type {
|
|
11
|
+
ActionTrace,
|
|
11
12
|
ActionFailureCode,
|
|
12
13
|
ActionTargetResolved,
|
|
13
14
|
AdjustControlResponse,
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
WaitForUIChangeResponse,
|
|
19
20
|
UIElementSemanticMetadata,
|
|
20
21
|
UIElementState,
|
|
22
|
+
TraceStep,
|
|
21
23
|
TapElementResponse
|
|
22
24
|
} from '../types.js'
|
|
23
25
|
|
|
@@ -79,6 +81,39 @@ interface RankedResolutionCandidate {
|
|
|
79
81
|
interactable: boolean
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
function buildObservationTrace({
|
|
85
|
+
actionType,
|
|
86
|
+
stage,
|
|
87
|
+
success,
|
|
88
|
+
attempts,
|
|
89
|
+
metadata
|
|
90
|
+
}: {
|
|
91
|
+
actionType: string
|
|
92
|
+
stage: 'verify' | 'stabilize' | 'resolve'
|
|
93
|
+
success: boolean
|
|
94
|
+
attempts: number
|
|
95
|
+
metadata?: Record<string, unknown>
|
|
96
|
+
}): ActionTrace {
|
|
97
|
+
const now = Date.now()
|
|
98
|
+
const actionId = nextActionId(actionType, now)
|
|
99
|
+
const steps: TraceStep[] = [
|
|
100
|
+
createTraceStep({
|
|
101
|
+
stage,
|
|
102
|
+
timestamp: now,
|
|
103
|
+
result: success ? 'success' : 'failure',
|
|
104
|
+
attemptIndex: 0,
|
|
105
|
+
metadata
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
action_id: actionId,
|
|
111
|
+
steps,
|
|
112
|
+
final_outcome: success ? 'success' : 'failure',
|
|
113
|
+
attempts: Math.max(1, Math.floor(attempts || 1))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
interface FindElementResolutionSummary {
|
|
83
118
|
confidence: number
|
|
84
119
|
reason: string
|
|
@@ -742,6 +777,22 @@ export class ToolsInteract {
|
|
|
742
777
|
let resolvedDeviceId = deviceId
|
|
743
778
|
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
744
779
|
let semanticFallbackElement: FindElementResponse['element'] | null = null
|
|
780
|
+
const traceSteps: TraceStep[] = []
|
|
781
|
+
let traceAttemptIndex = 0
|
|
782
|
+
|
|
783
|
+
const recordTraceStep = (
|
|
784
|
+
stage: TraceStep['stage'],
|
|
785
|
+
result: TraceStep['result'],
|
|
786
|
+
metadata?: Record<string, unknown>
|
|
787
|
+
) => {
|
|
788
|
+
traceSteps.push(createTraceStep({
|
|
789
|
+
stage,
|
|
790
|
+
timestamp: Date.now(),
|
|
791
|
+
result,
|
|
792
|
+
attemptIndex: traceAttemptIndex++,
|
|
793
|
+
metadata
|
|
794
|
+
}))
|
|
795
|
+
}
|
|
745
796
|
|
|
746
797
|
const buildFailure = (
|
|
747
798
|
failureCode: ActionFailureCode,
|
|
@@ -754,6 +805,22 @@ export class ToolsInteract {
|
|
|
754
805
|
retryable = false,
|
|
755
806
|
uiFingerprintAfter: string | null = null
|
|
756
807
|
): AdjustControlResponse => {
|
|
808
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
809
|
+
recordTraceStep('resolve', 'failure',
|
|
810
|
+
{
|
|
811
|
+
reason,
|
|
812
|
+
failure_code: failureCode
|
|
813
|
+
})
|
|
814
|
+
}
|
|
815
|
+
if (!traceSteps.some((step) => step.stage === 'recover')) {
|
|
816
|
+
recordTraceStep('recover', retryable ? 'retry' : 'failure', {
|
|
817
|
+
reason,
|
|
818
|
+
failure_code: failureCode,
|
|
819
|
+
retry_allowed: retryable,
|
|
820
|
+
recovery_attempts: attempts,
|
|
821
|
+
retry_depth: attempts
|
|
822
|
+
})
|
|
823
|
+
}
|
|
757
824
|
const base = buildActionExecutionResult({
|
|
758
825
|
actionType,
|
|
759
826
|
sourceModule: 'interact',
|
|
@@ -774,7 +841,8 @@ export class ToolsInteract {
|
|
|
774
841
|
converged: false,
|
|
775
842
|
within_tolerance: false,
|
|
776
843
|
reason
|
|
777
|
-
}
|
|
844
|
+
},
|
|
845
|
+
traceSteps
|
|
778
846
|
}) as AdjustControlResponse
|
|
779
847
|
|
|
780
848
|
return {
|
|
@@ -999,11 +1067,25 @@ export class ToolsInteract {
|
|
|
999
1067
|
|
|
1000
1068
|
lastObservedState = actualState
|
|
1001
1069
|
|
|
1070
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
1071
|
+
recordTraceStep('resolve', 'success', {
|
|
1072
|
+
resolved_target: resolvedTarget,
|
|
1073
|
+
current_value: currentValue,
|
|
1074
|
+
adjustment_mode: lastAdjustmentMode
|
|
1075
|
+
})
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1002
1078
|
if (property !== 'value' && property !== 'raw_value') {
|
|
1003
1079
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1004
1080
|
}
|
|
1005
1081
|
|
|
1006
1082
|
if (currentValue !== null && Math.abs(currentValue - targetValue) <= normalizedTolerance) {
|
|
1083
|
+
recordTraceStep('verify', 'success', {
|
|
1084
|
+
property,
|
|
1085
|
+
target_value: targetValue,
|
|
1086
|
+
actual_state: actualState,
|
|
1087
|
+
reason: 'control already within tolerance'
|
|
1088
|
+
})
|
|
1007
1089
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
1008
1090
|
const base = buildActionExecutionResult({
|
|
1009
1091
|
actionType,
|
|
@@ -1024,7 +1106,8 @@ export class ToolsInteract {
|
|
|
1024
1106
|
converged: true,
|
|
1025
1107
|
within_tolerance: true,
|
|
1026
1108
|
reason: 'control already within tolerance'
|
|
1027
|
-
}
|
|
1109
|
+
},
|
|
1110
|
+
traceSteps
|
|
1028
1111
|
}) as AdjustControlResponse
|
|
1029
1112
|
|
|
1030
1113
|
return {
|
|
@@ -1109,6 +1192,11 @@ export class ToolsInteract {
|
|
|
1109
1192
|
for (let i = 0; i < probePoints.length; i++) {
|
|
1110
1193
|
const probePoint = probePoints[i]
|
|
1111
1194
|
lastAdjustmentMode = 'coordinate'
|
|
1195
|
+
recordTraceStep('execute', 'retry', {
|
|
1196
|
+
attempt: attemptCount + 1,
|
|
1197
|
+
mode: 'coordinate',
|
|
1198
|
+
point: probePoint
|
|
1199
|
+
})
|
|
1112
1200
|
const actionResult = await ToolsInteract.tapHandler({
|
|
1113
1201
|
platform: resolvedPlatform,
|
|
1114
1202
|
x: probePoint.x,
|
|
@@ -1119,12 +1207,25 @@ export class ToolsInteract {
|
|
|
1119
1207
|
actionDevice = actionResult.device ?? actionDevice
|
|
1120
1208
|
|
|
1121
1209
|
if (!actionResult.success) {
|
|
1210
|
+
recordTraceStep('execute', 'retry', {
|
|
1211
|
+
attempt: attemptCount,
|
|
1212
|
+
mode: 'coordinate',
|
|
1213
|
+
point: probePoint,
|
|
1214
|
+
success: false
|
|
1215
|
+
})
|
|
1122
1216
|
continue
|
|
1123
1217
|
}
|
|
1124
1218
|
|
|
1125
1219
|
verificationResult = await runVerification()
|
|
1126
1220
|
observedState = verificationResult.observedState
|
|
1127
1221
|
lastObservedState = observedState
|
|
1222
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1223
|
+
attempt: attemptCount,
|
|
1224
|
+
property,
|
|
1225
|
+
target_value: targetValue,
|
|
1226
|
+
actual_state: observedState,
|
|
1227
|
+
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
1228
|
+
})
|
|
1128
1229
|
|
|
1129
1230
|
if (verificationResult.withinTolerance) {
|
|
1130
1231
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
@@ -1147,7 +1248,8 @@ export class ToolsInteract {
|
|
|
1147
1248
|
converged: true,
|
|
1148
1249
|
within_tolerance: true,
|
|
1149
1250
|
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1150
|
-
}
|
|
1251
|
+
},
|
|
1252
|
+
traceSteps
|
|
1151
1253
|
}) as AdjustControlResponse
|
|
1152
1254
|
|
|
1153
1255
|
return {
|
|
@@ -1168,6 +1270,12 @@ export class ToolsInteract {
|
|
|
1168
1270
|
|
|
1169
1271
|
if (currentValue !== null) {
|
|
1170
1272
|
lastAdjustmentMode = 'gesture'
|
|
1273
|
+
recordTraceStep('execute', 'retry', {
|
|
1274
|
+
attempt: attemptCount + 1,
|
|
1275
|
+
mode: 'gesture',
|
|
1276
|
+
start: currentPoint,
|
|
1277
|
+
end: targetPoint
|
|
1278
|
+
})
|
|
1171
1279
|
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
1172
1280
|
platform: resolvedPlatform,
|
|
1173
1281
|
x1: currentPoint.x,
|
|
@@ -1179,6 +1287,13 @@ export class ToolsInteract {
|
|
|
1179
1287
|
})
|
|
1180
1288
|
attemptCount++
|
|
1181
1289
|
if (!fallbackActionResult.success) {
|
|
1290
|
+
recordTraceStep('execute', 'failure', {
|
|
1291
|
+
attempt: attemptCount,
|
|
1292
|
+
mode: 'gesture',
|
|
1293
|
+
start: currentPoint,
|
|
1294
|
+
end: targetPoint,
|
|
1295
|
+
success: false
|
|
1296
|
+
})
|
|
1182
1297
|
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device ?? actionDevice, observedState ?? actualState, attemptCount, lastAdjustmentMode, false)
|
|
1183
1298
|
}
|
|
1184
1299
|
|
|
@@ -1186,6 +1301,13 @@ export class ToolsInteract {
|
|
|
1186
1301
|
verificationResult = await runVerification()
|
|
1187
1302
|
observedState = verificationResult.observedState
|
|
1188
1303
|
lastObservedState = observedState
|
|
1304
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1305
|
+
attempt: attemptCount,
|
|
1306
|
+
property,
|
|
1307
|
+
target_value: targetValue,
|
|
1308
|
+
actual_state: observedState,
|
|
1309
|
+
reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
|
|
1310
|
+
})
|
|
1189
1311
|
|
|
1190
1312
|
if (verificationResult.withinTolerance) {
|
|
1191
1313
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
@@ -1208,7 +1330,8 @@ export class ToolsInteract {
|
|
|
1208
1330
|
converged: true,
|
|
1209
1331
|
within_tolerance: true,
|
|
1210
1332
|
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1211
|
-
}
|
|
1333
|
+
},
|
|
1334
|
+
traceSteps
|
|
1212
1335
|
}) as AdjustControlResponse
|
|
1213
1336
|
|
|
1214
1337
|
return {
|
|
@@ -1231,6 +1354,13 @@ export class ToolsInteract {
|
|
|
1231
1354
|
lastObservedState = observedState
|
|
1232
1355
|
|
|
1233
1356
|
if (verificationResult.withinTolerance) {
|
|
1357
|
+
recordTraceStep('verify', 'success', {
|
|
1358
|
+
attempt: attemptCount,
|
|
1359
|
+
property,
|
|
1360
|
+
target_value: targetValue,
|
|
1361
|
+
actual_state: observedState,
|
|
1362
|
+
reason: verification?.reason ?? 'control converged to target value'
|
|
1363
|
+
})
|
|
1234
1364
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
1235
1365
|
const base = buildActionExecutionResult({
|
|
1236
1366
|
actionType,
|
|
@@ -1251,7 +1381,8 @@ export class ToolsInteract {
|
|
|
1251
1381
|
converged: true,
|
|
1252
1382
|
within_tolerance: true,
|
|
1253
1383
|
reason: verification?.reason ?? 'control converged to target value'
|
|
1254
|
-
}
|
|
1384
|
+
},
|
|
1385
|
+
traceSteps
|
|
1255
1386
|
}) as AdjustControlResponse
|
|
1256
1387
|
|
|
1257
1388
|
return {
|
|
@@ -2023,7 +2154,24 @@ export class ToolsInteract {
|
|
|
2023
2154
|
basis,
|
|
2024
2155
|
matched: success,
|
|
2025
2156
|
reason
|
|
2026
|
-
}
|
|
2157
|
+
},
|
|
2158
|
+
trace: buildObservationTrace({
|
|
2159
|
+
actionType: 'expect_screen',
|
|
2160
|
+
stage: 'verify',
|
|
2161
|
+
success,
|
|
2162
|
+
attempts: 1,
|
|
2163
|
+
metadata: {
|
|
2164
|
+
expected_screen: expectedScreen,
|
|
2165
|
+
observed_screen: {
|
|
2166
|
+
fingerprint: observedScreen.fingerprint,
|
|
2167
|
+
screen: observedScreenLabel
|
|
2168
|
+
},
|
|
2169
|
+
comparison: {
|
|
2170
|
+
basis,
|
|
2171
|
+
matched: success
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
})
|
|
2027
2175
|
}
|
|
2028
2176
|
}
|
|
2029
2177
|
|
|
@@ -2093,7 +2241,18 @@ export class ToolsInteract {
|
|
|
2093
2241
|
semantic: (result.element as any).semantic ?? null
|
|
2094
2242
|
}
|
|
2095
2243
|
},
|
|
2096
|
-
reason: 'selector is visible'
|
|
2244
|
+
reason: 'selector is visible',
|
|
2245
|
+
trace: buildObservationTrace({
|
|
2246
|
+
actionType: 'expect_element_visible',
|
|
2247
|
+
stage: 'verify',
|
|
2248
|
+
success: true,
|
|
2249
|
+
attempts: 1,
|
|
2250
|
+
metadata: {
|
|
2251
|
+
selector,
|
|
2252
|
+
element_id: result.element.elementId ?? element_id ?? null,
|
|
2253
|
+
status: result.status
|
|
2254
|
+
}
|
|
2255
|
+
})
|
|
2097
2256
|
}
|
|
2098
2257
|
}
|
|
2099
2258
|
|
|
@@ -2112,7 +2271,19 @@ export class ToolsInteract {
|
|
|
2112
2271
|
},
|
|
2113
2272
|
reason: result?.error?.message ?? 'selector is not visible',
|
|
2114
2273
|
failure_code: errorCode,
|
|
2115
|
-
retryable: errorCode === 'TIMEOUT'
|
|
2274
|
+
retryable: errorCode === 'TIMEOUT',
|
|
2275
|
+
trace: buildObservationTrace({
|
|
2276
|
+
actionType: 'expect_element_visible',
|
|
2277
|
+
stage: 'verify',
|
|
2278
|
+
success: false,
|
|
2279
|
+
attempts: 1,
|
|
2280
|
+
metadata: {
|
|
2281
|
+
selector,
|
|
2282
|
+
element_id: element_id ?? null,
|
|
2283
|
+
status: result?.status ?? null,
|
|
2284
|
+
reason: result?.error?.message ?? 'selector is not visible'
|
|
2285
|
+
}
|
|
2286
|
+
})
|
|
2116
2287
|
}
|
|
2117
2288
|
}
|
|
2118
2289
|
|
|
@@ -2157,6 +2328,30 @@ export class ToolsInteract {
|
|
|
2157
2328
|
let lastObservedValue: boolean | number | string | Record<string, unknown> | null = null
|
|
2158
2329
|
let lastRawValue: boolean | number | string | null = null
|
|
2159
2330
|
let lastResolvedElementId: string | null = element_id ?? null
|
|
2331
|
+
const traceSteps: TraceStep[] = []
|
|
2332
|
+
let traceAttemptIndex = 0
|
|
2333
|
+
let resolveRecorded = false
|
|
2334
|
+
|
|
2335
|
+
const recordTraceStep = (
|
|
2336
|
+
stage: TraceStep['stage'],
|
|
2337
|
+
result: TraceStep['result'],
|
|
2338
|
+
metadata?: Record<string, unknown>
|
|
2339
|
+
) => {
|
|
2340
|
+
traceSteps.push(createTraceStep({
|
|
2341
|
+
stage,
|
|
2342
|
+
timestamp: Date.now(),
|
|
2343
|
+
result,
|
|
2344
|
+
attemptIndex: traceAttemptIndex++,
|
|
2345
|
+
metadata
|
|
2346
|
+
}))
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
const buildStateTrace = (outcome: 'success' | 'failure'): ActionTrace => ({
|
|
2350
|
+
action_id: nextActionId('expect_state', Date.now()),
|
|
2351
|
+
steps: traceSteps,
|
|
2352
|
+
final_outcome: outcome,
|
|
2353
|
+
attempts
|
|
2354
|
+
})
|
|
2160
2355
|
|
|
2161
2356
|
while (Date.now() <= deadline) {
|
|
2162
2357
|
attempts++
|
|
@@ -2165,7 +2360,6 @@ export class ToolsInteract {
|
|
|
2165
2360
|
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android')
|
|
2166
2361
|
const treeDeviceId = tree?.device?.id || deviceId
|
|
2167
2362
|
const treeAgeMs = typeof tree?.captured_at_ms === 'number' ? Date.now() - tree.captured_at_ms : null
|
|
2168
|
-
|
|
2169
2363
|
let matched: { el: UiElement, idx: number } | null = null
|
|
2170
2364
|
|
|
2171
2365
|
if (element_id) {
|
|
@@ -2184,6 +2378,19 @@ export class ToolsInteract {
|
|
|
2184
2378
|
lastReason = 'element not found'
|
|
2185
2379
|
lastFailureCode = 'ELEMENT_NOT_FOUND'
|
|
2186
2380
|
stableCount = 0
|
|
2381
|
+
recordTraceStep('resolve', 'retry', {
|
|
2382
|
+
selector: selector ?? null,
|
|
2383
|
+
element_id: lastResolvedElementId,
|
|
2384
|
+
matched: false,
|
|
2385
|
+
reason: lastReason,
|
|
2386
|
+
attempt: attempts
|
|
2387
|
+
})
|
|
2388
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2389
|
+
stabilization_attempts: attempts,
|
|
2390
|
+
stable_observation_count: stableCount,
|
|
2391
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2392
|
+
reason: lastReason
|
|
2393
|
+
})
|
|
2187
2394
|
await sleep(pollDelay)
|
|
2188
2395
|
continue
|
|
2189
2396
|
}
|
|
@@ -2200,10 +2407,33 @@ export class ToolsInteract {
|
|
|
2200
2407
|
lastReason = 'stale snapshot'
|
|
2201
2408
|
lastFailureCode = 'UNKNOWN'
|
|
2202
2409
|
stableCount = 0
|
|
2410
|
+
recordTraceStep('resolve', 'retry', {
|
|
2411
|
+
selector: selector ?? null,
|
|
2412
|
+
element_id: lastResolvedElementId,
|
|
2413
|
+
matched: true,
|
|
2414
|
+
reason: lastReason,
|
|
2415
|
+
attempt: attempts
|
|
2416
|
+
})
|
|
2417
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2418
|
+
stabilization_attempts: attempts,
|
|
2419
|
+
stable_observation_count: stableCount,
|
|
2420
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2421
|
+
reason: lastReason
|
|
2422
|
+
})
|
|
2203
2423
|
await sleep(pollDelay)
|
|
2204
2424
|
continue
|
|
2205
2425
|
}
|
|
2206
2426
|
|
|
2427
|
+
if (!resolveRecorded) {
|
|
2428
|
+
recordTraceStep('resolve', 'success', {
|
|
2429
|
+
selector: selector ?? null,
|
|
2430
|
+
element_id: lastResolvedElementId,
|
|
2431
|
+
matched: true,
|
|
2432
|
+
reason: 'element resolved'
|
|
2433
|
+
})
|
|
2434
|
+
resolveRecorded = true
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2207
2437
|
const observedState = matched.el.state ?? null
|
|
2208
2438
|
const actual = observedState?.[property as keyof UIElementState] ?? null
|
|
2209
2439
|
|
|
@@ -2320,6 +2550,18 @@ export class ToolsInteract {
|
|
|
2320
2550
|
|
|
2321
2551
|
if (success) {
|
|
2322
2552
|
stableCount++
|
|
2553
|
+
recordTraceStep('stabilize', 'success', {
|
|
2554
|
+
stabilization_attempts: attempts,
|
|
2555
|
+
stable_observation_count: stableCount,
|
|
2556
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2557
|
+
reason
|
|
2558
|
+
})
|
|
2559
|
+
recordTraceStep('verify', 'success', {
|
|
2560
|
+
property,
|
|
2561
|
+
expected,
|
|
2562
|
+
observed_value: observedValue,
|
|
2563
|
+
reason
|
|
2564
|
+
})
|
|
2323
2565
|
if (stableCount >= stableTarget) {
|
|
2324
2566
|
return {
|
|
2325
2567
|
success: true,
|
|
@@ -2336,7 +2578,8 @@ export class ToolsInteract {
|
|
|
2336
2578
|
stabilization_attempts: attempts,
|
|
2337
2579
|
stabilization_window_ms: Date.now() - start,
|
|
2338
2580
|
stable_observation_count: stableCount,
|
|
2339
|
-
snapshot_freshness_ms: treeAgeMs ?? undefined
|
|
2581
|
+
snapshot_freshness_ms: treeAgeMs ?? undefined,
|
|
2582
|
+
trace: buildStateTrace('success')
|
|
2340
2583
|
} as ExpectStateResponse & {
|
|
2341
2584
|
stabilization_attempts?: number;
|
|
2342
2585
|
stabilization_window_ms?: number;
|
|
@@ -2348,6 +2591,18 @@ export class ToolsInteract {
|
|
|
2348
2591
|
stableCount = 0
|
|
2349
2592
|
lastReason = reason || lastReason
|
|
2350
2593
|
lastFailureCode = 'UNKNOWN'
|
|
2594
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2595
|
+
stabilization_attempts: attempts,
|
|
2596
|
+
stable_observation_count: stableCount,
|
|
2597
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2598
|
+
reason: lastReason
|
|
2599
|
+
})
|
|
2600
|
+
recordTraceStep('verify', 'retry', {
|
|
2601
|
+
property,
|
|
2602
|
+
expected,
|
|
2603
|
+
observed_value: observedValue,
|
|
2604
|
+
reason: lastReason
|
|
2605
|
+
})
|
|
2351
2606
|
}
|
|
2352
2607
|
|
|
2353
2608
|
if (!success) {
|
|
@@ -2371,7 +2626,8 @@ export class ToolsInteract {
|
|
|
2371
2626
|
},
|
|
2372
2627
|
reason: lastReason,
|
|
2373
2628
|
failure_code: lastFailureCode,
|
|
2374
|
-
retryable: true
|
|
2629
|
+
retryable: true,
|
|
2630
|
+
trace: buildStateTrace('failure')
|
|
2375
2631
|
}
|
|
2376
2632
|
}
|
|
2377
2633
|
|