humanish 0.76.0 → 0.78.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";
@@ -885,8 +885,19 @@ export function desktopBrowserFamily(value) {
885
885
  * The observer still degrades to `{}` for the loop; the callback is how a lane says out loud that
886
886
  * url/text criteria are not being measured, instead of letting the funnel report 0/N (#514).
887
887
  */
888
- 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) {
889
898
  let reported = false;
899
+ let drifted = false;
900
+ const checkedTargets = new Set(drift === undefined ? [] : [drift.emulatedTargetId]);
890
901
  const unavailable = (reason) => {
891
902
  if (!reported) {
892
903
  reported = true;
@@ -894,6 +905,29 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoi
894
905
  }
895
906
  return {};
896
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
+ };
897
931
  return async () => {
898
932
  const result = await desktop.commands.run(chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "active", mode: "state" }), { requestTimeoutMs, timeoutMs: 5_000 });
899
933
  if (result.exitCode !== undefined && result.exitCode !== 0) {
@@ -902,6 +936,8 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoi
902
936
  const parsed = parseChromeCdpProbeOutput(result.stdout);
903
937
  if (parsed.unavailable !== undefined)
904
938
  return unavailable(parsed.unavailable);
939
+ if (parsed.targetId !== undefined)
940
+ await checkLaterTarget(parsed.targetId);
905
941
  return {
906
942
  ...(parsed.url === undefined ? {} : { url: parsed.url }),
907
943
  ...(parsed.title === undefined ? {} : { title: parsed.title }),
@@ -994,9 +1030,10 @@ export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint,
994
1030
  touch: request.touch,
995
1031
  userAgent: request.userAgent
996
1032
  };
1033
+ const emulatedTargetId = applied.targetId ?? fidelityRead.targetId;
997
1034
  if (fidelityRead.fidelity === undefined) {
998
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.`);
999
- return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [] }, warnings };
1036
+ return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [] }, warnings, holderName, ...(emulatedTargetId === undefined ? {} : { targetId: emulatedTargetId }) };
1000
1037
  }
1001
1038
  const resolved = { ...fidelityRead.fidelity, source: "cdp" };
1002
1039
  if (resolved.innerWidth !== request.width) {
@@ -1011,7 +1048,7 @@ export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint,
1011
1048
  if (!resolved.userAgent.includes("Mobile") && !resolved.userAgent.includes("Android") && !resolved.userAgent.includes("iPhone")) {
1012
1049
  warnings.push("Mobile emulation requested a mobile user agent; the page reports a desktop one.");
1013
1050
  }
1014
- return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [], resolved }, warnings };
1051
+ return { fidelity: { tier: "mobile-emulated", requested, applied: applied.applied ?? [], resolved }, warnings, holderName, ...(emulatedTargetId === undefined ? {} : { targetId: emulatedTargetId }) };
1015
1052
  }
1016
1053
  function isMeasuredRect(value) {
1017
1054
  if (!value || typeof value !== "object")
@@ -1291,12 +1328,16 @@ function stripResolvedArcSegments(text) {
1291
1328
  })
1292
1329
  .join(" ");
1293
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/;
1294
1333
  function stripNegatedNonBlockerPhrases(text) {
1295
1334
  return text
1296
1335
  // FIRST, before the narrower rules eat the "no blockers" and leave "encountered ... error"
1297
1336
  // behind: "I encountered no blockers or unclear error output." refused a clean passing run on
1298
1337
  // 2026-09-01. A verb of encounter followed by "no" negates the whole clause, so drop the clause.
1299
- .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 : " ")
1300
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, "")
1301
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, "")
1302
1343
  .replace(/\bnot\s+(?:blocked|a blocker|an error|failed)\b/g, "")
@@ -1460,6 +1501,8 @@ export async function runCuaLane(spec, deps) {
1460
1501
  let browserLaunched = false;
1461
1502
  let initialBrowserGeometry;
1462
1503
  let appliedFidelity;
1504
+ let emulatedTargetId;
1505
+ let emulationHolderName;
1463
1506
  let browserWindowId;
1464
1507
  let browserTargetId;
1465
1508
  const declaredScreen = declaredScreenForRender(spec.devicePreset, spec.deviceName, spec.resolution);
@@ -1512,7 +1555,17 @@ export async function runCuaLane(spec, deps) {
1512
1555
  resolution: spec.resolution,
1513
1556
  dpi: 96,
1514
1557
  lifecycle: { onTimeout: "kill" }
1515
- }, 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
+ });
1516
1569
  sandboxId = desktop.sandboxId;
1517
1570
  // #358 salvage: journal the id to disk before any work — an interrupted run reclaims by
1518
1571
  // exact recorded id (`humanish reclaim`), never by enumerating the account.
@@ -1687,6 +1740,8 @@ export async function runCuaLane(spec, deps) {
1687
1740
  userAgent: fidelityRequest.userAgent ?? DEFAULT_MOBILE_USER_AGENT
1688
1741
  });
1689
1742
  appliedFidelity = applied.fidelity;
1743
+ emulatedTargetId = applied.targetId;
1744
+ emulationHolderName = applied.holderName;
1690
1745
  warnings.push(...applied.warnings);
1691
1746
  }
1692
1747
  }
@@ -1828,7 +1883,26 @@ export async function runCuaLane(spec, deps) {
1828
1883
  (reason) => {
1829
1884
  warnings.push(`Browser-state observer unavailable for lane ${spec.laneId} (${redactText(deps.scrubKnownValues(reason))}); ` +
1830
1885
  "urlIncludes/urlPathEquals/textIncludes stop conditions and task criteria are NOT being measured this session.");
1831
- })
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
+ })
1832
1906
  }
1833
1907
  }
1834
1908
  : {}),
@@ -1909,6 +1983,15 @@ export async function runCuaLane(spec, deps) {
1909
1983
  : initialBrowserGeometry ?? finalGeometry;
1910
1984
  const geometryWarnings = [...new Set(chosenGeometry.warnings.map((warning) => deps.scrubKnownValues(warning)))];
1911
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
+ }
1912
1995
  desktopGeometry = {
1913
1996
  screen: desktopGeometry.screen,
1914
1997
  ...(chosenGeometry.browserWindow === undefined ? {} : { browserWindow: chosenGeometry.browserWindow }),
@@ -3555,9 +3638,12 @@ export async function provisionLocalTreeSubject(desktop, args) {
3555
3638
  const uploadStartedAt = now();
3556
3639
  emitPhaseStarted(args.onPhase, now, "upload", "uploading packed local-tree archive");
3557
3640
  try {
3558
- await desktop.files.write(LOCAL_TREE_REMOTE_ARCHIVE_PATH, args.archiveBuffer, {
3641
+ await withOneRetryOnTransientE2BError(() => desktop.files.write(LOCAL_TREE_REMOTE_ARCHIVE_PATH, args.archiveBuffer, {
3559
3642
  requestTimeoutMs: args.requestTimeoutMs,
3560
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 })
3561
3647
  });
3562
3648
  }
3563
3649
  catch (error) {