@deeeed/metamask-harness 0.44.0 → 0.44.2
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 +81 -9
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +5 -0
- package/dist/app-lifecycle.js +56 -0
- package/dist/runner.js +134 -23
- package/library/actions/mobile/perps/performance-capture.mjs +7 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +22 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.44.2 - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- Read the selected Mobile target identity and its broker generation for `status-selected` without attaching to CDP.
|
|
12
|
+
|
|
13
|
+
## 0.44.1 - 2026-08-27
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Accept retained market context as lifecycle-valid Homepage evidence during account-only switches.
|
|
18
|
+
- Require controller-backed Mobile unlock proof before continuing a recipe after startup or restart.
|
|
19
|
+
|
|
5
20
|
## 0.44.0 - 2026-08-25
|
|
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,16 +418,32 @@ 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;
|
|
420
429
|
var account = null;
|
|
430
|
+
var keyringUnlocked = false;
|
|
421
431
|
try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
|
|
422
|
-
|
|
432
|
+
try { keyringUnlocked = globalThis.Engine?.context?.KeyringController?.isUnlocked?.() === true; } catch(e) {}
|
|
433
|
+
return {
|
|
434
|
+
route: route,
|
|
435
|
+
account: account,
|
|
436
|
+
agenticPresent: agenticPresent,
|
|
437
|
+
keyringUnlocked: keyringUnlocked,
|
|
438
|
+
};
|
|
423
439
|
})()`;
|
|
424
440
|
const snapshot = await cdpEval(client, expr);
|
|
425
|
-
return {
|
|
441
|
+
return {
|
|
442
|
+
...snapshot,
|
|
443
|
+
...(runtimeIdentity ? { runtimeIdentity } : {}),
|
|
444
|
+
deviceName: deviceName || '',
|
|
445
|
+
platform: platform || '',
|
|
446
|
+
};
|
|
426
447
|
},
|
|
427
448
|
|
|
428
449
|
async 'list-accounts'(client) {
|
|
@@ -1352,7 +1373,10 @@ Environment:
|
|
|
1352
1373
|
).trim();
|
|
1353
1374
|
}
|
|
1354
1375
|
|
|
1355
|
-
async function brokerTargets({
|
|
1376
|
+
async function brokerTargets({
|
|
1377
|
+
retainIdentity = false,
|
|
1378
|
+
includeGeneration = false,
|
|
1379
|
+
} = {}) {
|
|
1356
1380
|
const discoveryClient = await createBrokerClient(
|
|
1357
1381
|
brokerSocketFile,
|
|
1358
1382
|
'',
|
|
@@ -1360,14 +1384,22 @@ Environment:
|
|
|
1360
1384
|
);
|
|
1361
1385
|
try {
|
|
1362
1386
|
const targets = await discoveryClient.control(
|
|
1363
|
-
retainIdentity
|
|
1387
|
+
retainIdentity
|
|
1388
|
+
? 'resolve-targets'
|
|
1389
|
+
: includeGeneration
|
|
1390
|
+
? 'list-target-identities'
|
|
1391
|
+
: 'list-targets',
|
|
1364
1392
|
retainIdentity ? { nameIncludes: selectedTargetPin() } : {},
|
|
1365
1393
|
timeout,
|
|
1366
1394
|
);
|
|
1367
1395
|
return (Array.isArray(targets) ? targets : []).flatMap((target) => {
|
|
1368
1396
|
const deviceId = String(target?.deviceId || '');
|
|
1369
1397
|
return deviceId
|
|
1370
|
-
? [{
|
|
1398
|
+
? [{
|
|
1399
|
+
wsUrl: deviceId,
|
|
1400
|
+
deviceName: String(target?.name || ''),
|
|
1401
|
+
generation: Number(target?.generation) || 0,
|
|
1402
|
+
}]
|
|
1371
1403
|
: [];
|
|
1372
1404
|
});
|
|
1373
1405
|
} finally {
|
|
@@ -1383,13 +1415,31 @@ Environment:
|
|
|
1383
1415
|
)
|
|
1384
1416
|
: targets;
|
|
1385
1417
|
if (candidates.length !== 1) {
|
|
1386
|
-
|
|
1418
|
+
const error = new Error(
|
|
1387
1419
|
`Mobile CDP broker target selection requires one target; found ${candidates.length}${pin ? ` for ${JSON.stringify(pin)}` : ''}.`,
|
|
1388
1420
|
);
|
|
1421
|
+
throw candidates.length === 0
|
|
1422
|
+
? coded(error, BRIDGE_ERROR_CODES.NO_TARGET)
|
|
1423
|
+
: error;
|
|
1389
1424
|
}
|
|
1390
1425
|
return candidates[0];
|
|
1391
1426
|
}
|
|
1392
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
|
+
|
|
1393
1443
|
async function clientFor(wsUrl) {
|
|
1394
1444
|
if (brokerAvailable) {
|
|
1395
1445
|
return createBrokerClient(
|
|
@@ -1432,9 +1482,10 @@ Environment:
|
|
|
1432
1482
|
return;
|
|
1433
1483
|
}
|
|
1434
1484
|
|
|
1435
|
-
const
|
|
1485
|
+
const selectedTarget = brokerAvailable
|
|
1436
1486
|
? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
|
|
1437
1487
|
: await discoverTarget(port, { probe: true });
|
|
1488
|
+
const { wsUrl, deviceName, generation } = selectedTarget;
|
|
1438
1489
|
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1439
1490
|
const client = await clientFor(wsUrl);
|
|
1440
1491
|
|
|
@@ -1442,7 +1493,28 @@ Environment:
|
|
|
1442
1493
|
const platform = command.endsWith('-deferred')
|
|
1443
1494
|
? ''
|
|
1444
1495
|
: await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1445
|
-
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
|
+
}
|
|
1446
1518
|
console.log(JSON.stringify(result, null, 2));
|
|
1447
1519
|
} finally {
|
|
1448
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/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) {
|
|
@@ -680,6 +680,13 @@ function frameTime(record) {
|
|
|
680
680
|
|
|
681
681
|
function frameIsFresh(record) {
|
|
682
682
|
if (record.fresh_for_lifecycle === false) return false;
|
|
683
|
+
if (record.source === "retained_market_context") {
|
|
684
|
+
return (
|
|
685
|
+
record.lifecycle === "account_switch" &&
|
|
686
|
+
!isAccountContentVariant(record.content_variant) &&
|
|
687
|
+
record.fresh_for_lifecycle === true
|
|
688
|
+
);
|
|
689
|
+
}
|
|
683
690
|
return (
|
|
684
691
|
FRESH_SOURCES.has(record.source) ||
|
|
685
692
|
((record.source === "resident_state" || record.source === "memory_cache") &&
|
|
@@ -94,7 +94,7 @@ async function waitForWalletState(input, initialStatus, timeoutMs) {
|
|
|
94
94
|
const deadline = Date.now() + timeoutMs;
|
|
95
95
|
let last = initialStatus;
|
|
96
96
|
while (true) {
|
|
97
|
-
if (
|
|
97
|
+
if (isUnlockedStatus(last, input) || routeName(last, input) === 'Login') return last;
|
|
98
98
|
if (Date.now() >= deadline) break;
|
|
99
99
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
100
100
|
try {
|
|
@@ -104,6 +104,11 @@ async function waitForWalletState(input, initialStatus, timeoutMs) {
|
|
|
104
104
|
// still bounds this loop and the final error reports the last wallet state.
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
if (selectedAccount(last, input)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Mobile wallet route and controller unlock state did not settle within the remaining ${timeoutMs}ms target budget (route ${routeName(last, input) || 'unknown'}).`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
107
112
|
throw new Error(
|
|
108
113
|
`No wallet onboarded on this device (route ${routeName(last, input) || 'unknown'}, empty wallet status) — unlock has nothing to unlock.\n Next: mm-harness fixtures set # applies the fixture wallet, then re-run: mm-harness call ensure_unlocked`,
|
|
109
114
|
);
|
|
@@ -116,7 +121,7 @@ async function waitForUnlocked(input, timeoutMs = 30000) {
|
|
|
116
121
|
while (Date.now() < deadline) {
|
|
117
122
|
try {
|
|
118
123
|
last = await statusBeforeDeadline(input, deadline);
|
|
119
|
-
if (
|
|
124
|
+
if (isUnlockedStatus(last, input)) return last;
|
|
120
125
|
} catch (error) {
|
|
121
126
|
lastError = error;
|
|
122
127
|
}
|
|
@@ -126,7 +131,14 @@ async function waitForUnlocked(input, timeoutMs = 30000) {
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
function isUnlockedStatus(status, input) {
|
|
129
|
-
|
|
134
|
+
const selected = selectedStatus(status, input);
|
|
135
|
+
const route = routeName(status, input);
|
|
136
|
+
return (
|
|
137
|
+
Boolean(selectedAccount(status, input)) &&
|
|
138
|
+
selected?.keyringUnlocked === true &&
|
|
139
|
+
route.length > 0 &&
|
|
140
|
+
route !== 'Login'
|
|
141
|
+
);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
async function waitForStableUnlocked(input, initialStatus, stableMs = 750) {
|
|
@@ -188,6 +200,8 @@ export async function ensureUnlocked(input) {
|
|
|
188
200
|
action: input.action,
|
|
189
201
|
unlocked: true,
|
|
190
202
|
alreadyUnlocked: true,
|
|
203
|
+
keyringUnlocked:
|
|
204
|
+
selectedStatus(stableBefore, input)?.keyringUnlocked === true,
|
|
191
205
|
account: selectedAccount(stableBefore, input),
|
|
192
206
|
deviceName: selectedStatus(stableBefore, input)?.deviceName ?? null,
|
|
193
207
|
platform: selectedStatus(stableBefore, input)?.platform ?? null,
|
|
@@ -209,11 +223,13 @@ export async function ensureUnlocked(input) {
|
|
|
209
223
|
}
|
|
210
224
|
try {
|
|
211
225
|
const current = await statusBeforeDeadline(input, unlockDeadline);
|
|
212
|
-
if (
|
|
226
|
+
if (isUnlockedStatus(current, input)) {
|
|
213
227
|
return {
|
|
214
228
|
action: input.action,
|
|
215
229
|
unlocked: true,
|
|
216
230
|
alreadyUnlocked: true,
|
|
231
|
+
keyringUnlocked:
|
|
232
|
+
selectedStatus(current, input)?.keyringUnlocked === true,
|
|
217
233
|
account: selectedAccount(current, input),
|
|
218
234
|
deviceName: selectedStatus(current, input)?.deviceName ?? null,
|
|
219
235
|
platform: selectedStatus(current, input)?.platform ?? null,
|
|
@@ -246,6 +262,8 @@ export async function ensureUnlocked(input) {
|
|
|
246
262
|
return {
|
|
247
263
|
action: input.action,
|
|
248
264
|
unlocked: Boolean(result?.ok ?? result?.unlocked ?? true),
|
|
265
|
+
keyringUnlocked:
|
|
266
|
+
selectedStatus(stableAfter, input)?.keyringUnlocked === true,
|
|
249
267
|
account: selectedAccount(stableAfter, input),
|
|
250
268
|
route: selectedStatus(stableAfter, input)?.route ?? null,
|
|
251
269
|
deviceName: selectedStatus(stableAfter, input)?.deviceName ?? null,
|