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.
- package/README.md +30 -11
- package/dist/chrome-cdp-probe.d.ts +34 -2
- package/dist/chrome-cdp-probe.js +291 -56
- package/dist/chrome-cdp-probe.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +35 -1
- package/dist/cua-actor-lab.js +220 -12
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-desktop-launch.d.ts +33 -1
- package/dist/e2b-desktop-launch.js +49 -4
- package/dist/e2b-desktop-launch.js.map +1 -1
- package/dist/feedback.d.ts +20 -5
- package/dist/feedback.js +47 -16
- package/dist/feedback.js.map +1 -1
- package/dist/lab-config.d.ts +21 -0
- package/dist/lab-config.js +30 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/openai-responses-cu.d.ts +14 -0
- package/dist/openai-responses-cu.js +55 -2
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/program.js +26 -8
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +44 -0
- package/dist/run.js.map +1 -1
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +12 -0
- package/dist/telemetry.js.map +1 -1
- package/docs/contracts/schemas.md +4 -2
- package/docs/goals/current.md +39 -2
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.js
CHANGED
|
@@ -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 (
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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) {
|