humanish 0.75.0 → 0.77.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.
@@ -34,7 +34,7 @@ import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
34
34
  import { createLocalAgentProvider, detectLocalAgents } from "./local-agent-cli.js";
35
35
  import { startAppServerSession } from "./local-agent-appserver.js";
36
36
  import { startClaudeSession } from "./local-agent-claude-session.js";
37
- import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
37
+ import { createDesktopSandbox, withOneRetryOnTransientE2BError, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
38
38
  import { probeUrl, readDetachedLog, runDetachedStep, startDetachedProcess } from "./e2b-detached.js";
39
39
  import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, collectExternalCommsThread, deployCommsCatch, externalCatchHealthy, externalInboxUrl, refreshInboxSurface, writeInboxSurface } from "./comms-sandbox-catch.js";
40
40
  import { FakeInbox } from "./comms-fake-inbox.js";
@@ -736,10 +736,13 @@ async function fillDesktopBrowserWindow(desktop, windowId, resolution, requestTi
736
736
  })
737
737
  .catch(() => undefined);
738
738
  }
739
- async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, browserPreference) {
739
+ async function openDesktopBrowserTarget(desktop, targetUrl, requestTimeoutMs, browserPreference,
740
+ /** Launch-time flags that make mobile fidelity (#221) hold across every tab: the user agent and
741
+ * touch events are browser-wide here, where the CDP holder covers only the launch page. */
742
+ extraChromiumFlags = []) {
740
743
  const requestedBrowser = browserPreference ?? "default";
741
744
  if (isHttpUrl(targetUrl)) {
742
- const chromiumFlags = CHROMIUM_EVIDENCE_HYGIENE_FLAGS.map(shellSingleQuote).join(" ");
745
+ const chromiumFlags = [...CHROMIUM_EVIDENCE_HYGIENE_FLAGS, ...extraChromiumFlags].map(shellSingleQuote).join(" ");
743
746
  const browserLaunchCommand = [
744
747
  "set -euo pipefail",
745
748
  `target_url=${shellSingleQuote(targetUrl)}`,
@@ -882,8 +885,19 @@ export function desktopBrowserFamily(value) {
882
885
  * The observer still degrades to `{}` for the loop; the callback is how a lane says out loud that
883
886
  * url/text criteria are not being measured, instead of letting the funnel report 0/N (#514).
884
887
  */
885
- export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoint, targetId, onUnavailable) {
888
+ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoint, targetId, onUnavailable,
889
+ /**
890
+ * Mobile emulation on later tabs (#623): the holder attaches to every page target Chrome opens
891
+ * after the launch page, so a tab the participant opens later should lay out at the phone width
892
+ * too. The first observation on each new target reads that page's OWN report; a target that
893
+ * reports the requested width is recorded through `onCovered`, and one that does not (or cannot
894
+ * be read) fires `onDrift` once, so a phone-labelled lane that spent part of its session at
895
+ * desktop layout says so with the number the page gave.
896
+ */
897
+ drift) {
886
898
  let reported = false;
899
+ let drifted = false;
900
+ const checkedTargets = new Set(drift === undefined ? [] : [drift.emulatedTargetId]);
887
901
  const unavailable = (reason) => {
888
902
  if (!reported) {
889
903
  reported = true;
@@ -891,6 +905,29 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoi
891
905
  }
892
906
  return {};
893
907
  };
908
+ const checkLaterTarget = async (newTargetId) => {
909
+ if (drift === undefined || checkedTargets.has(newTargetId))
910
+ return;
911
+ checkedTargets.add(newTargetId);
912
+ const read = await desktop.commands.run(chromeCdpProbeCommand({ ...endpoint, targetId: newTargetId, prefer: "pinned", mode: "fidelity" }), { requestTimeoutMs, timeoutMs: 5_000 });
913
+ const fidelity = read.exitCode !== undefined && read.exitCode !== 0 ? undefined : parseChromeCdpProbeOutput(read.stdout).fidelity;
914
+ if (fidelity !== undefined && fidelity.innerWidth === drift.expectedWidth) {
915
+ drift.onCovered?.(newTargetId, { innerWidth: fidelity.innerWidth, devicePixelRatio: fidelity.devicePixelRatio, maxTouchPoints: fidelity.maxTouchPoints });
916
+ if (drift.expectTouch === true && fidelity.maxTouchPoints === 0 && !drifted) {
917
+ // The viewport followed; touch did not (yet): the holder reloads a later tab once after its
918
+ // first navigation commits, and this observation may have landed before that reload.
919
+ drifted = true;
920
+ drift.onDrift(`a later page target reports the ${fidelity.innerWidth} px viewport but navigator.maxTouchPoints 0 on its first observation; touch reaches a document only when it loads under the override`);
921
+ }
922
+ return;
923
+ }
924
+ if (drifted)
925
+ return;
926
+ drifted = true;
927
+ drift.onDrift(fidelity === undefined
928
+ ? "the participant drove a page target other than the emulated launch tab and that page's own read-back could not be taken; whether it laid out at the phone width is not known"
929
+ : `the participant drove a page target other than the emulated launch tab and that page reports a ${fidelity.innerWidth} px viewport where ${drift.expectedWidth} px was requested (DPR ${fidelity.devicePixelRatio}); the mobile user agent and touch events are browser-wide, the viewport override was not re-applied to it`);
930
+ };
894
931
  return async () => {
895
932
  const result = await desktop.commands.run(chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "active", mode: "state" }), { requestTimeoutMs, timeoutMs: 5_000 });
896
933
  if (result.exitCode !== undefined && result.exitCode !== 0) {
@@ -899,6 +936,8 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoi
899
936
  const parsed = parseChromeCdpProbeOutput(result.stdout);
900
937
  if (parsed.unavailable !== undefined)
901
938
  return unavailable(parsed.unavailable);
939
+ if (parsed.targetId !== undefined)
940
+ await checkLaterTarget(parsed.targetId);
902
941
  return {
903
942
  ...(parsed.url === undefined ? {} : { url: parsed.url }),
904
943
  ...(parsed.title === undefined ? {} : { title: parsed.title }),
@@ -937,6 +976,80 @@ export function makeChromeDesktopGeometryObserver(desktop, requestTimeoutMs, end
937
976
  };
938
977
  };
939
978
  }
979
+ /** The user agent a mobile-emulated lane presents unless the lab sets its own. */
980
+ export const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
981
+ /**
982
+ * Apply mobile emulation (#221) to the lane's launch page and read back what the page reports.
983
+ * Fails CLOSED: a request that cannot be applied throws, because a desktop run labelled mobile is
984
+ * the over-trust this feature exists to prevent. A read-back that cannot be taken is a warning
985
+ * (the emulation was applied; only the proof is missing).
986
+ */
987
+ export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint, targetId, request) {
988
+ const command = (mode) => chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "pinned", mode, emulation: request });
989
+ const read = async () => {
990
+ const result = await desktop.commands.run(command("fidelity"), { requestTimeoutMs, timeoutMs: 15_000 });
991
+ if (result.exitCode !== undefined && result.exitCode !== 0) {
992
+ return { unavailable: `probe exited ${result.exitCode}: ${tailOf(result.stderr ?? result.stdout ?? "")}` };
993
+ }
994
+ return parseChromeCdpProbeOutput(result.stdout);
995
+ };
996
+ // The UA / touch / DPR overrides are bound to the DevTools session that set them and lapse the
997
+ // moment its socket closes (measured: only the viewport width survived a one-shot apply). So the
998
+ // applier stays attached for the lane's whole life as a detached process; the sandbox teardown
999
+ // ends it. Its first stdout line says what was applied.
1000
+ const holderName = `mobile-emulation-${Date.now().toString(36)}`;
1001
+ await startDetachedProcess(desktop, { name: holderName, command: command("hold"), requestTimeoutMs });
1002
+ let announced;
1003
+ for (let attempt = 0; attempt < 30 && announced === undefined; attempt += 1) {
1004
+ await new Promise((resolve) => setTimeout(resolve, 500));
1005
+ const log = await readDetachedLog(desktop, holderName, requestTimeoutMs).catch(() => "");
1006
+ const line = log.split("\n").find((candidate) => candidate.trim().startsWith("{"));
1007
+ if (line !== undefined)
1008
+ announced = parseChromeCdpProbeOutput(line);
1009
+ }
1010
+ if (announced === undefined) {
1011
+ throw new Error("mobile emulation could not be applied: the in-sandbox applier printed nothing within 15 s");
1012
+ }
1013
+ if (announced.unavailable !== undefined) {
1014
+ throw new Error(`mobile emulation could not be applied (${announced.unavailable}); applied before failing: ${(announced.applied ?? []).join(", ") || "nothing"}`);
1015
+ }
1016
+ const applied = announced;
1017
+ const warnings = [];
1018
+ // The reload inside the applier takes a moment; the read-back is retried until the page reports
1019
+ // the requested viewport and user agent, so a slow page does not read as "no proof".
1020
+ let readBack = await read();
1021
+ for (let attempt = 0; attempt < 20 && (readBack.fidelity === undefined || readBack.fidelity.innerWidth !== request.width || !readBack.fidelity.userAgent.includes(request.userAgent.slice(0, 24))); attempt += 1) {
1022
+ await new Promise((resolve) => setTimeout(resolve, 500));
1023
+ readBack = await read();
1024
+ }
1025
+ const fidelityRead = readBack;
1026
+ const requested = {
1027
+ width: request.width,
1028
+ height: request.height,
1029
+ deviceScaleFactor: request.deviceScaleFactor,
1030
+ touch: request.touch,
1031
+ userAgent: request.userAgent
1032
+ };
1033
+ const emulatedTargetId = applied.targetId ?? fidelityRead.targetId;
1034
+ if (fidelityRead.fidelity === undefined) {
1035
+ warnings.push(`Mobile emulation was applied but the page's own report could not be read (${fidelityRead.unavailable ?? "no fidelity read"}); desktopGeometry.fidelity carries the request without a resolved block.`);
1036
+ return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [] }, warnings, holderName, ...(emulatedTargetId === undefined ? {} : { targetId: emulatedTargetId }) };
1037
+ }
1038
+ const resolved = { ...fidelityRead.fidelity, source: "cdp" };
1039
+ if (resolved.innerWidth !== request.width) {
1040
+ warnings.push(`Mobile emulation requested a ${request.width} px viewport; the page reports ${resolved.innerWidth} px.`);
1041
+ }
1042
+ if (resolved.devicePixelRatio !== request.deviceScaleFactor) {
1043
+ warnings.push(`Mobile emulation requested devicePixelRatio ${request.deviceScaleFactor}; the page reports ${resolved.devicePixelRatio}.`);
1044
+ }
1045
+ if (request.touch && resolved.maxTouchPoints === 0) {
1046
+ warnings.push("Mobile emulation requested touch; the page reports navigator.maxTouchPoints 0.");
1047
+ }
1048
+ if (!resolved.userAgent.includes("Mobile") && !resolved.userAgent.includes("Android") && !resolved.userAgent.includes("iPhone")) {
1049
+ warnings.push("Mobile emulation requested a mobile user agent; the page reports a desktop one.");
1050
+ }
1051
+ return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [], resolved }, warnings, holderName, ...(emulatedTargetId === undefined ? {} : { targetId: emulatedTargetId }) };
1052
+ }
940
1053
  function isMeasuredRect(value) {
941
1054
  if (!value || typeof value !== "object")
942
1055
  return false;
@@ -1018,11 +1131,14 @@ export async function captureDesktopBrowserGeometry(args) {
1018
1131
  : undefined;
1019
1132
  const browserWindow = chromeGeometry?.browserWindow ?? xdotoolWindow;
1020
1133
  const viewport = chromeGeometry?.viewport;
1134
+ // The fill check reads the X window when it was measured: under mobile emulation (#221) the
1135
+ // page's window.outerWidth reports the EMULATED screen (414), which is not a fill failure.
1136
+ const fillBounds = xdotoolWindow ?? browserWindow;
1021
1137
  if (!browserWindow) {
1022
1138
  warnings.push(`Browser outer bounds could not be measured for lane ${args.laneId}.`);
1023
1139
  }
1024
- else if (browserWindow.width !== args.requestedScreen[0] || browserWindow.height !== args.requestedScreen[1]) {
1025
- warnings.push(`Browser window fill did not reach the requested ${args.requestedScreen[0]}x${args.requestedScreen[1]} screen for lane ${args.laneId}; measured outer bounds are ${browserWindow.width}x${browserWindow.height}.`);
1140
+ else if (fillBounds !== undefined && (fillBounds.width !== args.requestedScreen[0] || fillBounds.height !== args.requestedScreen[1])) {
1141
+ warnings.push(`Browser window fill did not reach the requested ${args.requestedScreen[0]}x${args.requestedScreen[1]} screen for lane ${args.laneId}; measured outer bounds are ${fillBounds.width}x${fillBounds.height}.`);
1026
1142
  }
1027
1143
  if (!viewport) {
1028
1144
  // Name the cause, not only the symptom: the same dead DevTools channel that loses the viewport
@@ -1165,8 +1281,17 @@ function hasBlockerLanguage(text) {
1165
1281
  // study completed without a participant-reported finding". Friction is the INCLUSIVE scan; a
1166
1282
  // false positive here adds a candidate a person then reads, which is the cheap direction.
1167
1283
  const REPORTED_DEFECT_LANGUAGE = /\b(defects?|bugs?|accessibilit(y|ies)|inaccessible|not (keyboard|screen.?reader)[- ]?accessible|confus(ed|ing)|hesitat(ed|ion)|unexpected(ly)?|unclear|hard to (find|tell|see|read|reach)|no (visible )?focus|overlap(ped|ping|s)?|truncat(ed|es|ion)|cut off|did nothing|nothing happened|no effect)\b/;
1284
+ // The friction scan's own negations (#614). "Nothing was confusing", "no defects", "not unclear"
1285
+ // are what a participant writes when it has NOTHING to report, and until 2026-09-03 each of them
1286
+ // counted as reported friction and became a feedback candidate whose "actual" was a sentence
1287
+ // reporting no problem. Only the report-shaped adjectives are negatable here: "no visible focus",
1288
+ // "not keyboard-accessible" and "did nothing" are defects and stay.
1289
+ const NEGATED_REPORT_LANGUAGE = /\b(?:nothing|no|not|never|without|none)\s+(?:was\s+|were\s+|felt\s+|seemed\s+|really\s+|particularly\s+|especially\s+|major\s+|real\s+|obvious\s+|noticeable\s+)*(?:confus(?:ed|ing|ion)|unclear|unexpected(?:ly)?|hesitat(?:ed|ion|ions)|surpris(?:ed|ing|es)|defects?|bugs?|overlap(?:ped|ping|s)?|truncat(?:ed|es|ion)|hard to (?:find|tell|see|read|reach))\b/g;
1290
+ function stripNegatedReportLanguage(text) {
1291
+ return text.replace(NEGATED_REPORT_LANGUAGE, " ");
1292
+ }
1168
1293
  function completionReasonContradictsGoal(reason) {
1169
- const text = stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase()));
1294
+ const text = stripQuotedSpans(stripNegatedReportLanguage(stripNegatedNonBlockerPhrases(reason.toLowerCase())));
1170
1295
  return hasBlockerLanguage(text) || REPORTED_DEFECT_LANGUAGE.test(text);
1171
1296
  }
1172
1297
  /** The verdict scan (strict): like the friction scan, but resolved-arc segments are stripped
@@ -1203,12 +1328,16 @@ function stripResolvedArcSegments(text) {
1203
1328
  })
1204
1329
  .join(" ");
1205
1330
  }
1331
+ // Negations that DESCRIBE a defect rather than deny one. Kept out of every clause drop below.
1332
+ const DEFECT_SHAPED_NEGATION = /\bno\s+(?:visible\s+)?focus\b|\bno\s+(?:keyboard|screen.?reader)[- ]?(?:access|path|route|way|alternative|equivalent)|\bnot\s+(?:keyboard|screen.?reader)[- ]?accessible\b|\bno\s+(?:effect|feedback|response)\b|\b(?:did|does)\s+nothing\b|\bnothing\s+happened\b/;
1206
1333
  function stripNegatedNonBlockerPhrases(text) {
1207
1334
  return text
1208
1335
  // FIRST, before the narrower rules eat the "no blockers" and leave "encountered ... error"
1209
1336
  // behind: "I encountered no blockers or unclear error output." refused a clean passing run on
1210
1337
  // 2026-09-01. A verb of encounter followed by "no" negates the whole clause, so drop the clause.
1211
- .replace(/\b(?:encountered|hit|saw|found|met|had|got|ran into)\s+no\s+[^.!?\n]*/g, " ")
1338
+ // ... unless the clause names a DEFECT: "the delete control had no visible focus" is a
1339
+ // finding, and the verb it happens to use must not decide whether it counts (#622).
1340
+ .replace(/\b(?:encountered|hit|saw|found|met|had|got|ran into)\s+no\s+[^.!?\n]*/g, (clause) => DEFECT_SHAPED_NEGATION.test(clause) ? clause : " ")
1212
1341
  .replace(/\bno\s+(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\s+(?:was\s+|were\s+)?(?:encountered|observed|found|hit|seen|reported|detected)\b/g, "")
1213
1342
  .replace(/\bwithout\s+(?:a\s+|any\s+)?(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\b/g, "")
1214
1343
  .replace(/\bnot\s+(?:blocked|a blocker|an error|failed)\b/g, "")
@@ -1371,6 +1500,9 @@ export async function runCuaLane(spec, deps) {
1371
1500
  let browserLaunchIdentity;
1372
1501
  let browserLaunched = false;
1373
1502
  let initialBrowserGeometry;
1503
+ let appliedFidelity;
1504
+ let emulatedTargetId;
1505
+ let emulationHolderName;
1374
1506
  let browserWindowId;
1375
1507
  let browserTargetId;
1376
1508
  const declaredScreen = declaredScreenForRender(spec.devicePreset, spec.deviceName, spec.resolution);
@@ -1423,7 +1555,17 @@ export async function runCuaLane(spec, deps) {
1423
1555
  resolution: spec.resolution,
1424
1556
  dpi: 96,
1425
1557
  lifecycle: { onTimeout: "kill" }
1426
- }, config.execution?.desktop?.template);
1558
+ }, config.execution?.desktop?.template, {
1559
+ // One retry on a transient provider error (a sandbox whose envd was not routable yet, an
1560
+ // API reply without a body). The failed attempt may have allocated a sandbox this process
1561
+ // never learned the id of; its own timeoutMs is what reclaims it, so the warning says so.
1562
+ onRetry: (reason) => {
1563
+ const named = redactText(deps.scrubKnownValues(reason));
1564
+ warnings.push(`Sandbox create for lane ${spec.laneId} retried once after a transient provider error (${named}); ` +
1565
+ `a sandbox the first attempt may have allocated is not known to this run and expires on the provider's ${Math.round(deps.perLaneSandboxMs / 60_000)}-minute timeout.`);
1566
+ onSubjectPhase({ at: new Date(deps.now()).toISOString(), type: "cua-lab.sandbox.create.retry", message: `sandbox create retried once (${named})` });
1567
+ }
1568
+ });
1427
1569
  sandboxId = desktop.sandboxId;
1428
1570
  // #358 salvage: journal the id to disk before any work — an interrupted run reclaims by
1429
1571
  // exact recorded id (`humanish reclaim`), never by enumerating the account.
@@ -1562,12 +1704,46 @@ export async function runCuaLane(spec, deps) {
1562
1704
  });
1563
1705
  }
1564
1706
  if (!desktopCliRoute) {
1565
- const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser);
1707
+ const requestedFidelity = config.execution?.desktop?.fidelity;
1708
+ const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser, requestedFidelity?.mobileEmulation && spec.devicePreset.isMobile
1709
+ ? [
1710
+ `--user-agent=${requestedFidelity.userAgent ?? DEFAULT_MOBILE_USER_AGENT}`,
1711
+ ...(requestedFidelity.touch === false ? [] : ["--touch-events=enabled"])
1712
+ ]
1713
+ : []);
1566
1714
  desktopBrowser = browserLaunch.evidence;
1567
1715
  launchedBrowserFamily = browserLaunch.family;
1568
1716
  browserLaunchIdentity = browserLaunch.identity;
1569
1717
  browserLaunched = true;
1570
1718
  await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
1719
+ // Mobile fidelity beyond viewport size (#221): applied to the launch page before the
1720
+ // geometry capture and the participant's first observation, OUTSIDE the stream/geometry
1721
+ // try below (whose catch degrades to a warning): a request that cannot be applied fails
1722
+ // the lane closed with the reason.
1723
+ // Only lanes on a mobile preset are emulated: a run-wide flag must not hand a desktop or
1724
+ // tablet lane an iPhone user agent (the first live proof did exactly that to the desktop
1725
+ // newcomer beside the phone lane). Those lanes carry no fidelity block, which is honest.
1726
+ const fidelityRequest = config.execution?.desktop?.fidelity;
1727
+ if (fidelityRequest?.mobileEmulation && spec.devicePreset.isMobile) {
1728
+ if (launchedBrowserFamily !== "chromium") {
1729
+ throw new Error(`execution.desktop.fidelity.mobileEmulation needs Chrome or Chromium on lane ${spec.laneId}; the launched browser family is ${launchedBrowserFamily}. Set execution.desktop.browser: chrome.`);
1730
+ }
1731
+ const applied = await applyMobileEmulation(desktop, deps.requestTimeoutMs, {
1732
+ ...(browserLaunchIdentity?.cdpPort === undefined ? {} : { cdpPort: browserLaunchIdentity.cdpPort }),
1733
+ ...(browserLaunchIdentity?.profileDir === undefined ? {} : { profileDir: browserLaunchIdentity.profileDir }),
1734
+ targetUrl
1735
+ }, browserTargetId, {
1736
+ width: spec.devicePreset.width,
1737
+ height: spec.devicePreset.height,
1738
+ deviceScaleFactor: fidelityRequest.deviceScaleFactor ?? spec.devicePreset.deviceScaleFactor,
1739
+ touch: fidelityRequest.touch ?? true,
1740
+ userAgent: fidelityRequest.userAgent ?? DEFAULT_MOBILE_USER_AGENT
1741
+ });
1742
+ appliedFidelity = applied.fidelity;
1743
+ emulatedTargetId = applied.targetId;
1744
+ emulationHolderName = applied.holderName;
1745
+ warnings.push(...applied.warnings);
1746
+ }
1571
1747
  }
1572
1748
  else {
1573
1749
  // A terminal window, opened the way the browser is opened on every other route: the
@@ -1707,7 +1883,26 @@ export async function runCuaLane(spec, deps) {
1707
1883
  (reason) => {
1708
1884
  warnings.push(`Browser-state observer unavailable for lane ${spec.laneId} (${redactText(deps.scrubKnownValues(reason))}); ` +
1709
1885
  "urlIncludes/urlPathEquals/textIncludes stop conditions and task criteria are NOT being measured this session.");
1710
- })
1886
+ }, emulatedTargetId === undefined
1887
+ ? undefined
1888
+ : {
1889
+ emulatedTargetId,
1890
+ expectedWidth: spec.devicePreset.width,
1891
+ expectTouch: appliedFidelity?.requested.touch === true,
1892
+ onDrift: (reason) => {
1893
+ warnings.push(`Mobile emulation drift on lane ${spec.laneId}: ${reason} (#623).`);
1894
+ },
1895
+ onCovered: (coveredTargetId, read) => {
1896
+ // A later tab the page itself reported at the phone width: evidence that
1897
+ // the emulation followed the participant (#623), kept on the bundle.
1898
+ if (appliedFidelity === undefined)
1899
+ return;
1900
+ appliedFidelity = {
1901
+ ...appliedFidelity,
1902
+ laterTargets: [...(appliedFidelity.laterTargets ?? []), { targetId: coveredTargetId, ...read }]
1903
+ };
1904
+ }
1905
+ })
1711
1906
  }
1712
1907
  }
1713
1908
  : {}),
@@ -1788,10 +1983,20 @@ export async function runCuaLane(spec, deps) {
1788
1983
  : initialBrowserGeometry ?? finalGeometry;
1789
1984
  const geometryWarnings = [...new Set(chosenGeometry.warnings.map((warning) => deps.scrubKnownValues(warning)))];
1790
1985
  warnings.push(...geometryWarnings);
1986
+ // The emulation holder's own log, after its announce line: which later targets it
1987
+ // attached to, what it sent, and any reply that came back as an error (#623). Read while
1988
+ // the sandbox is alive; the first live proof had no way to say what the holder did.
1989
+ if (appliedFidelity !== undefined && emulationHolderName !== undefined) {
1990
+ const holderLog = await readDetachedLog(desktop, emulationHolderName, deps.requestTimeoutMs).catch(() => "");
1991
+ const lines = holderLog.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("{")).slice(1, 51);
1992
+ if (lines.length > 0)
1993
+ appliedFidelity = { ...appliedFidelity, holderLog: lines.map((line) => deps.scrubKnownValues(line)) };
1994
+ }
1791
1995
  desktopGeometry = {
1792
1996
  screen: desktopGeometry.screen,
1793
1997
  ...(chosenGeometry.browserWindow === undefined ? {} : { browserWindow: chosenGeometry.browserWindow }),
1794
1998
  ...(chosenGeometry.viewport === undefined ? {} : { viewport: chosenGeometry.viewport }),
1999
+ ...(appliedFidelity === undefined ? {} : { fidelity: appliedFidelity }),
1795
2000
  ...((desktopGeometry.warnings?.length ?? 0) + geometryWarnings.length === 0
1796
2001
  ? {}
1797
2002
  : { warnings: [...(desktopGeometry.warnings ?? []), ...geometryWarnings] })
@@ -3433,9 +3638,12 @@ export async function provisionLocalTreeSubject(desktop, args) {
3433
3638
  const uploadStartedAt = now();
3434
3639
  emitPhaseStarted(args.onPhase, now, "upload", "uploading packed local-tree archive");
3435
3640
  try {
3436
- await desktop.files.write(LOCAL_TREE_REMOTE_ARCHIVE_PATH, args.archiveBuffer, {
3641
+ await withOneRetryOnTransientE2BError(() => desktop.files.write(LOCAL_TREE_REMOTE_ARCHIVE_PATH, args.archiveBuffer, {
3437
3642
  requestTimeoutMs: args.requestTimeoutMs,
3438
3643
  useOctetStream: true
3644
+ }), {
3645
+ onRetry: (reason) => emitPhaseStarted(args.onPhase, now, "upload-retry", `local-tree archive upload retried once (${tailOf(args.scrub(reason))})`),
3646
+ ...(args.sleep === undefined ? {} : { sleep: args.sleep })
3439
3647
  });
3440
3648
  }
3441
3649
  catch (error) {