@deeeed/metamask-harness 0.44.1 → 0.44.3
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/CHANGELOG.md +15 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +5 -0
- package/dist/adapters.js +1 -1
- package/dist/app-lifecycle.js +56 -0
- package/dist/runner.js +134 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.44.3 - 2026-08-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Reuse one native UI provider session owner across recipe nodes on iOS so sequential gestures do not conflict with the retained XCTest runner, while each node still closes its logical session without shutting down the simulator.
|
|
10
|
+
- Update `@farmslot/recipe-harness` from 0.15.0 to 0.15.1 so unresolved relative Metro imports remain source bundle failures instead of triggering dependency installation during Mobile launch preflight.
|
|
11
|
+
|
|
12
|
+
## 0.44.2 - 2026-08-27
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Wait for a new React Native runtime after an authored Mobile restart, and avoid a second Android foreground launch when that runtime is already ready.
|
|
17
|
+
- Hold Mobile restart recovery inside one readiness deadline: the foreground relaunch and its subprocesses inherit the remaining budget, and an expired deadline fails the lifecycle action instead of starting another launch or readiness probe.
|
|
18
|
+
- Read the selected Mobile target identity and its broker generation for `status-selected` without attaching to CDP.
|
|
19
|
+
|
|
5
20
|
## 0.44.1 - 2026-08-27
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -30,9 +30,11 @@ const {
|
|
|
30
30
|
deviceIdFromUrl,
|
|
31
31
|
} = require('./lib/cdp-broker.cjs');
|
|
32
32
|
const {
|
|
33
|
+
BRIDGE_ERROR_CODES,
|
|
33
34
|
EXIT_CODE_BY_ERROR_CODE,
|
|
34
35
|
TEACHING_BY_ERROR_CODE,
|
|
35
36
|
classifyBridgeErrorMessage,
|
|
37
|
+
coded,
|
|
36
38
|
formatErrorMarker,
|
|
37
39
|
} = require('./lib/bridge-errors.cjs');
|
|
38
40
|
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
@@ -293,6 +295,9 @@ function parseHudStep(args) {
|
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
const COMMANDS = {
|
|
298
|
+
async 'target-identity-selected'() {
|
|
299
|
+
throw new Error('target-identity-selected is resolved before CDP attachment');
|
|
300
|
+
},
|
|
296
301
|
async navigate(client, args, { deviceName, platform } = {}) {
|
|
297
302
|
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
298
303
|
if (!routeName) {
|
|
@@ -413,7 +418,11 @@ const COMMANDS = {
|
|
|
413
418
|
return { currentRoute: route ?? null, deviceName, platform };
|
|
414
419
|
},
|
|
415
420
|
|
|
416
|
-
async status(
|
|
421
|
+
async status(
|
|
422
|
+
client,
|
|
423
|
+
_args,
|
|
424
|
+
{ deviceName, platform, runtimeIdentity } = {},
|
|
425
|
+
) {
|
|
417
426
|
const expr = `(function() {
|
|
418
427
|
var agenticPresent = typeof globalThis.__AGENTIC__ !== 'undefined';
|
|
419
428
|
var route = globalThis.__AGENTIC__?.getRoute() || null;
|
|
@@ -429,7 +438,12 @@ const COMMANDS = {
|
|
|
429
438
|
};
|
|
430
439
|
})()`;
|
|
431
440
|
const snapshot = await cdpEval(client, expr);
|
|
432
|
-
return {
|
|
441
|
+
return {
|
|
442
|
+
...snapshot,
|
|
443
|
+
...(runtimeIdentity ? { runtimeIdentity } : {}),
|
|
444
|
+
deviceName: deviceName || '',
|
|
445
|
+
platform: platform || '',
|
|
446
|
+
};
|
|
433
447
|
},
|
|
434
448
|
|
|
435
449
|
async 'list-accounts'(client) {
|
|
@@ -1359,7 +1373,10 @@ Environment:
|
|
|
1359
1373
|
).trim();
|
|
1360
1374
|
}
|
|
1361
1375
|
|
|
1362
|
-
async function brokerTargets({
|
|
1376
|
+
async function brokerTargets({
|
|
1377
|
+
retainIdentity = false,
|
|
1378
|
+
includeGeneration = false,
|
|
1379
|
+
} = {}) {
|
|
1363
1380
|
const discoveryClient = await createBrokerClient(
|
|
1364
1381
|
brokerSocketFile,
|
|
1365
1382
|
'',
|
|
@@ -1367,14 +1384,22 @@ Environment:
|
|
|
1367
1384
|
);
|
|
1368
1385
|
try {
|
|
1369
1386
|
const targets = await discoveryClient.control(
|
|
1370
|
-
retainIdentity
|
|
1387
|
+
retainIdentity
|
|
1388
|
+
? 'resolve-targets'
|
|
1389
|
+
: includeGeneration
|
|
1390
|
+
? 'list-target-identities'
|
|
1391
|
+
: 'list-targets',
|
|
1371
1392
|
retainIdentity ? { nameIncludes: selectedTargetPin() } : {},
|
|
1372
1393
|
timeout,
|
|
1373
1394
|
);
|
|
1374
1395
|
return (Array.isArray(targets) ? targets : []).flatMap((target) => {
|
|
1375
1396
|
const deviceId = String(target?.deviceId || '');
|
|
1376
1397
|
return deviceId
|
|
1377
|
-
? [{
|
|
1398
|
+
? [{
|
|
1399
|
+
wsUrl: deviceId,
|
|
1400
|
+
deviceName: String(target?.name || ''),
|
|
1401
|
+
generation: Number(target?.generation) || 0,
|
|
1402
|
+
}]
|
|
1378
1403
|
: [];
|
|
1379
1404
|
});
|
|
1380
1405
|
} finally {
|
|
@@ -1390,13 +1415,31 @@ Environment:
|
|
|
1390
1415
|
)
|
|
1391
1416
|
: targets;
|
|
1392
1417
|
if (candidates.length !== 1) {
|
|
1393
|
-
|
|
1418
|
+
const error = new Error(
|
|
1394
1419
|
`Mobile CDP broker target selection requires one target; found ${candidates.length}${pin ? ` for ${JSON.stringify(pin)}` : ''}.`,
|
|
1395
1420
|
);
|
|
1421
|
+
throw candidates.length === 0
|
|
1422
|
+
? coded(error, BRIDGE_ERROR_CODES.NO_TARGET)
|
|
1423
|
+
: error;
|
|
1396
1424
|
}
|
|
1397
1425
|
return candidates[0];
|
|
1398
1426
|
}
|
|
1399
1427
|
|
|
1428
|
+
if (command === 'target-identity-selected') {
|
|
1429
|
+
const target = brokerAvailable
|
|
1430
|
+
? selectBrokerTarget(await brokerTargets({ includeGeneration: true }))
|
|
1431
|
+
: await discoverTarget(port, { probe: true });
|
|
1432
|
+
const generation = Number(target.generation) || 0;
|
|
1433
|
+
console.log(
|
|
1434
|
+
JSON.stringify({
|
|
1435
|
+
runtimeIdentity: generation
|
|
1436
|
+
? `${target.wsUrl}:${generation}`
|
|
1437
|
+
: target.wsUrl,
|
|
1438
|
+
}),
|
|
1439
|
+
);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1400
1443
|
async function clientFor(wsUrl) {
|
|
1401
1444
|
if (brokerAvailable) {
|
|
1402
1445
|
return createBrokerClient(
|
|
@@ -1439,9 +1482,10 @@ Environment:
|
|
|
1439
1482
|
return;
|
|
1440
1483
|
}
|
|
1441
1484
|
|
|
1442
|
-
const
|
|
1485
|
+
const selectedTarget = brokerAvailable
|
|
1443
1486
|
? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
|
|
1444
1487
|
: await discoverTarget(port, { probe: true });
|
|
1488
|
+
const { wsUrl, deviceName, generation } = selectedTarget;
|
|
1445
1489
|
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1446
1490
|
const client = await clientFor(wsUrl);
|
|
1447
1491
|
|
|
@@ -1449,7 +1493,28 @@ Environment:
|
|
|
1449
1493
|
const platform = command.endsWith('-deferred')
|
|
1450
1494
|
? ''
|
|
1451
1495
|
: await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1452
|
-
const
|
|
1496
|
+
const runtimeIdentity =
|
|
1497
|
+
command === 'status-selected'
|
|
1498
|
+
? generation
|
|
1499
|
+
? `${wsUrl}:${generation}`
|
|
1500
|
+
: wsUrl
|
|
1501
|
+
: undefined;
|
|
1502
|
+
const result = await handler(client, args.slice(1), {
|
|
1503
|
+
deviceName,
|
|
1504
|
+
platform,
|
|
1505
|
+
runtimeIdentity,
|
|
1506
|
+
});
|
|
1507
|
+
if (brokerAvailable && command === 'status-selected') {
|
|
1508
|
+
const currentTarget = selectBrokerTarget(
|
|
1509
|
+
await brokerTargets({ includeGeneration: true }),
|
|
1510
|
+
);
|
|
1511
|
+
if (
|
|
1512
|
+
currentTarget.wsUrl !== wsUrl ||
|
|
1513
|
+
currentTarget.generation !== generation
|
|
1514
|
+
) {
|
|
1515
|
+
throw new Error('Hermes runtime rotated during status-selected');
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1453
1518
|
console.log(JSON.stringify(result, null, 2));
|
|
1454
1519
|
} finally {
|
|
1455
1520
|
client.close();
|
|
@@ -59,6 +59,7 @@ const NEEDLE_CODES = [
|
|
|
59
59
|
['did not match any metro target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
60
60
|
['pinned android device', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
61
61
|
['no react native bridge target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
62
|
+
['cdp broker target unavailable', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
62
63
|
['websocket closed', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
63
64
|
['websocket error', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
64
65
|
['cdp connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
@@ -613,6 +613,11 @@ function createCdpBroker({
|
|
|
613
613
|
name,
|
|
614
614
|
}));
|
|
615
615
|
}
|
|
616
|
+
if (action === 'list-target-identities') {
|
|
617
|
+
return targetList({ readyOnly: true }).map(
|
|
618
|
+
({ deviceId, generation, name }) => ({ deviceId, generation, name }),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
616
621
|
if (action === 'resolve-targets') {
|
|
617
622
|
return resolveTargets(params, timeoutMs, socket);
|
|
618
623
|
}
|
package/dist/adapters.js
CHANGED
|
@@ -955,7 +955,7 @@ async function executeNativeProviderAction(action, node, context, createTranspor
|
|
|
955
955
|
return executeAndroidCoordinateSwipe(node, String(device));
|
|
956
956
|
}
|
|
957
957
|
const app = platform === "android" ? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask" : context.env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
|
|
958
|
-
const session = `mm-harness-${process.pid}-${context.nodeId}
|
|
958
|
+
const session = (platform === "ios" ? `mm-harness-${process.pid}-${device}` : `mm-harness-${process.pid}-${context.nodeId}`).replace(/[^a-zA-Z0-9._-]/gu, "-");
|
|
959
959
|
const stateDir = nativeAgentDeviceStateDir(context.env ?? {}, context.projectRoot, String(device));
|
|
960
960
|
let transport;
|
|
961
961
|
try {
|
package/dist/app-lifecycle.js
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const MOBILE_LIFECYCLE_DEADLINE_FIELD = "_mobile_lifecycle_deadline_epoch_ms";
|
|
5
|
+
const lifecycleDeadline = new AsyncLocalStorage();
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
function bindMetaMaskMobileLifecycleDeadline(adapters) {
|
|
8
|
+
return adapters.map((adapter) => ({
|
|
9
|
+
...adapter,
|
|
10
|
+
execute(node, context) {
|
|
11
|
+
const deadline = Number(node[MOBILE_LIFECYCLE_DEADLINE_FIELD]);
|
|
12
|
+
if (!Number.isFinite(deadline)) return adapter.execute(node, context);
|
|
13
|
+
return lifecycleDeadline.run(deadline, () => adapter.execute(node, context));
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function createMetaMaskMobileLifecycleCommandRunner(base = defaultLifecycleCommandRunner()) {
|
|
18
|
+
return {
|
|
19
|
+
execFile(file, args, options) {
|
|
20
|
+
const deadline = lifecycleDeadline.getStore();
|
|
21
|
+
if (deadline === void 0) return base.execFile(file, args, options);
|
|
22
|
+
const remainingMs = deadline - Date.now();
|
|
23
|
+
if (remainingMs <= 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Mobile lifecycle deadline expired before ${file} could start.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const configuredTimeoutMs = options?.timeoutMs;
|
|
29
|
+
return base.execFile(file, args, {
|
|
30
|
+
timeoutMs: configuredTimeoutMs === void 0 ? remainingMs : Math.min(configuredTimeoutMs, remainingMs)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function defaultLifecycleCommandRunner() {
|
|
36
|
+
return {
|
|
37
|
+
async execFile(file, args, options) {
|
|
38
|
+
try {
|
|
39
|
+
return await execFileAsync(file, args, {
|
|
40
|
+
timeout: options?.timeoutMs,
|
|
41
|
+
encoding: "utf8"
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
const output = error;
|
|
46
|
+
const details = [output.message, output.stderr, output.stdout].filter(Boolean).join("\n");
|
|
47
|
+
throw new Error(`${file} ${args.join(" ")} failed: ${details}`);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
1
54
|
function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
|
|
2
55
|
const context = rawContext;
|
|
3
56
|
const platform = resolveMobilePlatform(node, context);
|
|
@@ -69,5 +122,8 @@ function scalar(value, label) {
|
|
|
69
122
|
throw new Error(`${label} must be a string, number, or boolean.`);
|
|
70
123
|
}
|
|
71
124
|
export {
|
|
125
|
+
MOBILE_LIFECYCLE_DEADLINE_FIELD,
|
|
126
|
+
bindMetaMaskMobileLifecycleDeadline,
|
|
127
|
+
createMetaMaskMobileLifecycleCommandRunner,
|
|
72
128
|
resolveMetaMaskMobileLifecycleTarget
|
|
73
129
|
};
|
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
3
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
bridgeCommand,
|
|
6
|
+
MOBILE_BRIDGE_ERROR_CODES
|
|
7
|
+
} from "../library/actions/mobile/platform/bridge.mjs";
|
|
5
8
|
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
6
9
|
import {
|
|
7
10
|
mobileSourceFingerprint,
|
|
@@ -13,7 +16,12 @@ import {
|
|
|
13
16
|
createAndroidVideoRecorder,
|
|
14
17
|
createIosSimulatorVideoRecorder
|
|
15
18
|
} from "./adapters/mobile/video-recorder.js";
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
bindMetaMaskMobileLifecycleDeadline,
|
|
21
|
+
createMetaMaskMobileLifecycleCommandRunner,
|
|
22
|
+
MOBILE_LIFECYCLE_DEADLINE_FIELD,
|
|
23
|
+
resolveMetaMaskMobileLifecycleTarget
|
|
24
|
+
} from "./app-lifecycle.js";
|
|
17
25
|
import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
|
|
18
26
|
import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
19
27
|
import {
|
|
@@ -84,10 +92,13 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
84
92
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
85
93
|
const lifecycle = mobileSourceAwareLifecycleAdapters(
|
|
86
94
|
adapter,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
bindMetaMaskMobileLifecycleDeadline(
|
|
96
|
+
createAppLifecycleAdapters({
|
|
97
|
+
actions,
|
|
98
|
+
commandRunner: createMetaMaskMobileLifecycleCommandRunner(),
|
|
99
|
+
targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
|
|
100
|
+
})
|
|
101
|
+
)
|
|
91
102
|
);
|
|
92
103
|
const custom = createMetaMaskAdapters(
|
|
93
104
|
adapter,
|
|
@@ -337,7 +348,7 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
337
348
|
}, processIdentity = {
|
|
338
349
|
readIosPid: readIosAppPid,
|
|
339
350
|
readAndroidPid: readAndroidAppPid
|
|
340
|
-
}) {
|
|
351
|
+
}, runtimeMarker = readMobileLifecycleRuntimeIdentity) {
|
|
341
352
|
if (adapter !== "mobile") return lifecycle;
|
|
342
353
|
return lifecycle.map((entry) => ({
|
|
343
354
|
...entry,
|
|
@@ -357,19 +368,62 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
357
368
|
const readProcessId = androidLifecycle ? processIdentity.readAndroidPid ?? readAndroidAppPid : processIdentity.readIosPid;
|
|
358
369
|
const processBefore = recordsProcessIdentity ? readProcessId(node, context) : void 0;
|
|
359
370
|
const fingerprint = recordsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
|
|
371
|
+
const previousRuntimeIdentity = recordsSource ? await runtimeMarker(node, context) : void 0;
|
|
360
372
|
const androidRestart = command === "restart" && androidLifecycle;
|
|
361
|
-
const
|
|
362
|
-
const
|
|
373
|
+
const initialResult = await entry.execute(node, context);
|
|
374
|
+
const readinessDeadline = reloadsSource ? mobileLifecycleReadinessDeadline(command, node) : void 0;
|
|
363
375
|
let result = initialResult;
|
|
376
|
+
let runtimeReady = false;
|
|
364
377
|
if (androidRestart) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
378
|
+
try {
|
|
379
|
+
const remainingMs = Math.max(
|
|
380
|
+
1,
|
|
381
|
+
(readinessDeadline ?? Date.now()) - Date.now()
|
|
382
|
+
);
|
|
383
|
+
const handoffDeadline = Math.min(
|
|
384
|
+
readinessDeadline ?? Number.POSITIVE_INFINITY,
|
|
385
|
+
Date.now() + Math.min(1e4, Math.max(1, remainingMs / 2))
|
|
386
|
+
);
|
|
387
|
+
await readinessProbe(
|
|
388
|
+
node,
|
|
389
|
+
context,
|
|
390
|
+
previousRuntimeIdentity,
|
|
391
|
+
handoffDeadline
|
|
392
|
+
);
|
|
393
|
+
runtimeReady = true;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
const foregroundTimeoutMs = Math.max(
|
|
399
|
+
1,
|
|
400
|
+
(readinessDeadline ?? Date.now() + 1) - Date.now()
|
|
401
|
+
);
|
|
402
|
+
const foregroundResult = await entry.execute(
|
|
403
|
+
{
|
|
404
|
+
...node,
|
|
405
|
+
command: "foreground",
|
|
406
|
+
settle_ms: 0,
|
|
407
|
+
timeout_ms: foregroundTimeoutMs,
|
|
408
|
+
[MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline
|
|
409
|
+
},
|
|
410
|
+
context
|
|
411
|
+
);
|
|
412
|
+
result = mergeAndroidRestartResults(initialResult, foregroundResult);
|
|
413
|
+
}
|
|
370
414
|
}
|
|
371
|
-
if (reloadsSource) {
|
|
372
|
-
|
|
415
|
+
if (reloadsSource && !runtimeReady) {
|
|
416
|
+
if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Mobile lifecycle ${String(command)} exhausted its runtime readiness deadline before recovery completed.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
await readinessProbe(
|
|
422
|
+
node,
|
|
423
|
+
context,
|
|
424
|
+
previousRuntimeIdentity,
|
|
425
|
+
readinessDeadline
|
|
426
|
+
);
|
|
373
427
|
}
|
|
374
428
|
if (recordsProcessIdentity) {
|
|
375
429
|
const processAfter = readProcessId(node, context);
|
|
@@ -468,35 +522,92 @@ function mergeAndroidRestartResults(initialResult, foregroundResult) {
|
|
|
468
522
|
}
|
|
469
523
|
};
|
|
470
524
|
}
|
|
471
|
-
async function probeMobileLifecycleRuntime(node, context) {
|
|
525
|
+
async function probeMobileLifecycleRuntime(node, context, previousRuntimeIdentity, deadlineMs) {
|
|
472
526
|
const command = node.command ?? node.event ?? node.state;
|
|
473
527
|
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
474
528
|
command,
|
|
475
529
|
node.runtime_ready_timeout_ms
|
|
476
530
|
);
|
|
477
|
-
const
|
|
531
|
+
const startedAt = Date.now();
|
|
532
|
+
const deadline = deadlineMs ?? startedAt + timeoutMs;
|
|
478
533
|
let lastError;
|
|
479
534
|
while (Date.now() < deadline) {
|
|
480
535
|
try {
|
|
481
|
-
|
|
536
|
+
const status = await bridgeCommand(
|
|
482
537
|
{
|
|
483
538
|
action: "app.lifecycle",
|
|
484
539
|
node: {
|
|
485
540
|
...node,
|
|
486
|
-
cdp_timeout_ms: Math.min(8e3, Math.max(
|
|
541
|
+
cdp_timeout_ms: Math.min(8e3, Math.max(1, deadline - Date.now()))
|
|
487
542
|
},
|
|
488
543
|
context
|
|
489
544
|
},
|
|
490
|
-
["
|
|
545
|
+
["status-selected"]
|
|
546
|
+
);
|
|
547
|
+
if (status?.agenticPresent === true && status.route && (!previousRuntimeIdentity || status.runtimeIdentity !== previousRuntimeIdentity)) {
|
|
548
|
+
return status;
|
|
549
|
+
}
|
|
550
|
+
lastError = new Error(
|
|
551
|
+
previousRuntimeIdentity && status?.runtimeIdentity === previousRuntimeIdentity ? "retiring Mobile runtime is still selected" : "selected Mobile runtime is not ready"
|
|
491
552
|
);
|
|
492
553
|
} catch (error) {
|
|
493
554
|
lastError = error;
|
|
494
|
-
|
|
555
|
+
}
|
|
556
|
+
const remainingMs = deadline - Date.now();
|
|
557
|
+
if (remainingMs > 0) {
|
|
558
|
+
await new Promise(
|
|
559
|
+
(resolve) => setTimeout(resolve, Math.min(1e3, remainingMs))
|
|
560
|
+
);
|
|
495
561
|
}
|
|
496
562
|
}
|
|
497
563
|
throw new Error(
|
|
498
|
-
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${
|
|
564
|
+
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${Math.max(0, deadline - startedAt)}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
async function readMobileLifecycleRuntimeIdentity(node, context) {
|
|
568
|
+
const deadline = Date.now() + 5e3;
|
|
569
|
+
let lastError;
|
|
570
|
+
do {
|
|
571
|
+
try {
|
|
572
|
+
const status = await bridgeCommand(
|
|
573
|
+
{
|
|
574
|
+
action: "app.lifecycle",
|
|
575
|
+
node: {
|
|
576
|
+
...node,
|
|
577
|
+
cdp_timeout_ms: Math.min(1e3, Math.max(1, deadline - Date.now()))
|
|
578
|
+
},
|
|
579
|
+
context
|
|
580
|
+
},
|
|
581
|
+
["target-identity-selected"]
|
|
582
|
+
);
|
|
583
|
+
if (typeof status?.runtimeIdentity === "string") {
|
|
584
|
+
return status.runtimeIdentity;
|
|
585
|
+
}
|
|
586
|
+
lastError = new Error("selected Mobile runtime has no stable identity");
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (error instanceof Error && "code" in error && error.code === MOBILE_BRIDGE_ERROR_CODES.NO_TARGET) {
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
lastError = error;
|
|
592
|
+
}
|
|
593
|
+
const remainingMs = deadline - Date.now();
|
|
594
|
+
if (remainingMs <= 0) {
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
await new Promise(
|
|
598
|
+
(resolve) => setTimeout(resolve, Math.min(250, remainingMs))
|
|
599
|
+
);
|
|
600
|
+
} while (Date.now() < deadline);
|
|
601
|
+
throw new Error(
|
|
602
|
+
`Mobile lifecycle restart could not identify the retiring runtime: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
function mobileLifecycleReadinessDeadline(command, node) {
|
|
606
|
+
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
607
|
+
command,
|
|
608
|
+
node.runtime_ready_timeout_ms
|
|
499
609
|
);
|
|
610
|
+
return Date.now() + timeoutMs;
|
|
500
611
|
}
|
|
501
612
|
function resolveMobileLifecycleReadinessTimeout(command, value) {
|
|
502
613
|
if (value === void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@farmslot/expo-recipe": "0.8.1",
|
|
25
25
|
"@farmslot/handoff": "^0.3.1",
|
|
26
26
|
"@farmslot/protocol": "0.21.0",
|
|
27
|
-
"@farmslot/recipe-harness": "0.15.
|
|
27
|
+
"@farmslot/recipe-harness": "0.15.1",
|
|
28
28
|
"agent-device": "0.19.3",
|
|
29
29
|
"commander": "^12.0.0",
|
|
30
30
|
"es-module-lexer": "2.3.1",
|