humanish 0.74.0 → 0.75.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/dist/chrome-cdp-probe.d.ts +63 -0
- package/dist/chrome-cdp-probe.js +288 -0
- package/dist/chrome-cdp-probe.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +16 -10
- package/dist/cua-actor-lab.js +123 -148
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/pricing.js +15 -6
- package/dist/pricing.js.map +1 -1
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +13 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.js
CHANGED
|
@@ -46,6 +46,7 @@ import { appendSandboxReceipt } from "./sandbox-receipts.js";
|
|
|
46
46
|
import { assertScreenshotEvidence } from "./image-evidence.js";
|
|
47
47
|
import { buildObserverData } from "./observer-data.js";
|
|
48
48
|
import { corepackCommandFor, needsNodeRuntime, nodeBootstrapCommand } from "./subject-runtime.js";
|
|
49
|
+
import { chromeCdpProbeCommand, parseChromeCdpProbeOutput } from "./chrome-cdp-probe.js";
|
|
49
50
|
import { personaToDirectives, renderPersonaPromptSection } from "./persona.js";
|
|
50
51
|
import { labPersonaIds, resolveCommittedPersonas } from "./persona-resolve.js";
|
|
51
52
|
import { renderTaskPrompt } from "./tasks.js";
|
|
@@ -505,6 +506,37 @@ function isoNow(now) {
|
|
|
505
506
|
return new Date(now()).toISOString();
|
|
506
507
|
}
|
|
507
508
|
/** Emit a phase-started event (no ok/durationMs: those belong to the matching completed event). */
|
|
509
|
+
/**
|
|
510
|
+
* Run a provisioning step and, when it fails with an EXIT CODE, run it once more (#602). A cold
|
|
511
|
+
* install of 0.74.0 lost its whole first live study to one transient TLS error inside the
|
|
512
|
+
* sandbox's `npm install`; the parallel install twenty seconds later passed, as had the ten
|
|
513
|
+
* before it. One retry clears that class. A TIMEOUT is not retried: its budget is already spent,
|
|
514
|
+
* and a second wait would double it. The retry runs under its own step name so both logs stay.
|
|
515
|
+
*/
|
|
516
|
+
async function runProvisioningStepWithOneRetry(desktop, args) {
|
|
517
|
+
const first = await runDetachedStep(desktop, {
|
|
518
|
+
name: args.name,
|
|
519
|
+
command: args.command,
|
|
520
|
+
cwd: args.cwd,
|
|
521
|
+
timeoutMs: args.timeoutMs,
|
|
522
|
+
requestTimeoutMs: args.requestTimeoutMs,
|
|
523
|
+
...args.timers
|
|
524
|
+
});
|
|
525
|
+
if (first.ok || first.timedOut)
|
|
526
|
+
return { ...first, attempts: 1 };
|
|
527
|
+
const retryStartedAt = args.now();
|
|
528
|
+
emitPhaseStarted(args.onPhase, args.now, args.retryPhase, `${args.retryMessage} (first attempt exited ${first.exitCode ?? "null"}; retrying once)`);
|
|
529
|
+
const second = await runDetachedStep(desktop, {
|
|
530
|
+
name: `${args.name}-retry`,
|
|
531
|
+
command: args.command,
|
|
532
|
+
cwd: args.cwd,
|
|
533
|
+
timeoutMs: args.timeoutMs,
|
|
534
|
+
requestTimeoutMs: args.requestTimeoutMs,
|
|
535
|
+
...args.timers
|
|
536
|
+
});
|
|
537
|
+
emitPhaseCompleted(args.onPhase, args.now, retryStartedAt, args.retryPhase, second.ok, second.ok ? `${args.retryMessage}: succeeded on the second attempt` : `${args.retryMessage}: failed twice`);
|
|
538
|
+
return { ...second, attempts: 2, ...(first.exitCode === undefined ? {} : { firstExitCode: first.exitCode }) };
|
|
539
|
+
}
|
|
508
540
|
function emitPhaseStarted(onPhase, now, phase, message) {
|
|
509
541
|
onPhase?.({ at: isoNow(now), type: `cua-lab.subject.${phase}.started`, message });
|
|
510
542
|
}
|
|
@@ -839,162 +871,70 @@ export function desktopBrowserFamily(value) {
|
|
|
839
871
|
return "unknown";
|
|
840
872
|
}
|
|
841
873
|
/**
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
"if (cdpPort === undefined && cdpProfileDir) {",
|
|
853
|
-
" try {",
|
|
854
|
-
" const { readFileSync } = await import('node:fs');",
|
|
855
|
-
" const marker = readFileSync(cdpProfileDir + '/DevToolsActivePort', 'utf8');",
|
|
856
|
-
" const parsed = Number.parseInt(String(marker.split('\\n')[0] ?? ''), 10);",
|
|
857
|
-
" if (Number.isInteger(parsed) && parsed > 0) cdpPort = parsed;",
|
|
858
|
-
" } catch {}",
|
|
859
|
-
"}",
|
|
860
|
-
"if (cdpPort === undefined) cdpPort = 9222;"
|
|
861
|
-
];
|
|
862
|
-
}
|
|
863
|
-
/** Shared CDP page-selection preamble: pinned target id first, then this lane's target URL,
|
|
864
|
-
* then a single-page fallback; never an arbitrary page from a multi-page endpoint. */
|
|
865
|
-
function chromeCdpPageSelectionScript(endpoint, targetId,
|
|
866
|
-
/**
|
|
867
|
-
* "pinned" (default): the launch-time target, for measurements about the ORIGINAL window
|
|
868
|
-
* (geometry). "active": the tab the participant is driving NOW — Chrome's /json lists page
|
|
869
|
-
* targets most-recently-focused first. The state observer must follow the participant: a
|
|
870
|
-
* verification link that opens in a NEW tab left the pinned observer reading the old tab
|
|
871
|
-
* forever, so the observed URL never changed again and stopWhen/task criteria went blind
|
|
872
|
-
* (a live run's funnel read reach-dashboard 0/2 under a screenshot OF the dashboard).
|
|
874
|
+
* The URL / title / page-text / scroll observer behind stopWhen and task criteria. One probe per
|
|
875
|
+
* observation, run on the sandbox's python3 (see chrome-cdp-probe.ts for why not node: #514).
|
|
876
|
+
*
|
|
877
|
+
* "active": follow the participant to whatever tab they are driving now — never pin the state
|
|
878
|
+
* observer to the launch tab (a verification link that opened in a NEW tab left a pinned observer
|
|
879
|
+
* reading the old tab forever).
|
|
880
|
+
*
|
|
881
|
+
* `onUnavailable` fires ONCE, on the first probe that could not read the page, with the reason.
|
|
882
|
+
* The observer still degrades to `{}` for the loop; the callback is how a lane says out loud that
|
|
883
|
+
* url/text criteria are not being measured, instead of letting the funnel report 0/N (#514).
|
|
873
884
|
*/
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
? "const page = httpPages[0] || (expectedTargetId ? httpPages.find((entry) => entry.id === expectedTargetId) : undefined);"
|
|
884
|
-
: "const page = expectedTargetId ? httpPages.find((entry) => entry.id === expectedTargetId) : (httpPages.find((entry) => normalizeUrl(entry.url) === normalizeUrl(expectedTargetUrl)) || (httpPages.length === 1 ? httpPages[0] : undefined));"
|
|
885
|
-
];
|
|
886
|
-
}
|
|
887
|
-
export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoint, targetId) {
|
|
885
|
+
export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoint, targetId, onUnavailable) {
|
|
886
|
+
let reported = false;
|
|
887
|
+
const unavailable = (reason) => {
|
|
888
|
+
if (!reported) {
|
|
889
|
+
reported = true;
|
|
890
|
+
onUnavailable?.(reason);
|
|
891
|
+
}
|
|
892
|
+
return {};
|
|
893
|
+
};
|
|
888
894
|
return async () => {
|
|
889
|
-
const
|
|
890
|
-
// "active": follow the participant to whatever tab they are driving now — never pin the
|
|
891
|
-
// state observer to the launch tab (see chromeCdpPageSelectionScript).
|
|
892
|
-
...chromeCdpPageSelectionScript(endpoint, targetId, "active"),
|
|
893
|
-
"if (!page) { console.log('{}'); process.exit(0); }",
|
|
894
|
-
"let text = '';",
|
|
895
|
-
"let scrollY = undefined;",
|
|
896
|
-
"let url = String(page.url || '');",
|
|
897
|
-
"let title = String(page.title || '');",
|
|
898
|
-
"if (typeof WebSocket === 'function' && page.webSocketDebuggerUrl) {",
|
|
899
|
-
" const ws = new WebSocket(page.webSocketDebuggerUrl);",
|
|
900
|
-
" const result = await new Promise((resolve) => {",
|
|
901
|
-
" const timer = setTimeout(() => resolve(undefined), 1500);",
|
|
902
|
-
" ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { returnByValue: true, expression: '({ url: location.href, title: document.title, text: (document.body && document.body.innerText || \"\").slice(0, 20000), scrollY: (window.scrollY || 0) })' } }));",
|
|
903
|
-
" ws.onmessage = (event) => {",
|
|
904
|
-
" try {",
|
|
905
|
-
" const payload = JSON.parse(String(event.data));",
|
|
906
|
-
" if (payload.id !== 1) return;",
|
|
907
|
-
" clearTimeout(timer);",
|
|
908
|
-
" resolve(payload.result && payload.result.result && payload.result.result.value);",
|
|
909
|
-
" } catch { clearTimeout(timer); resolve(undefined); }",
|
|
910
|
-
" };",
|
|
911
|
-
" ws.onerror = () => { clearTimeout(timer); resolve(undefined); };",
|
|
912
|
-
" }).finally(() => { try { ws.close(); } catch {} });",
|
|
913
|
-
" if (result && typeof result === 'object') {",
|
|
914
|
-
" url = typeof result.url === 'string' ? result.url : url;",
|
|
915
|
-
" title = typeof result.title === 'string' ? result.title : title;",
|
|
916
|
-
" text = typeof result.text === 'string' ? result.text : '';",
|
|
917
|
-
" scrollY = typeof result.scrollY === 'number' ? result.scrollY : undefined;",
|
|
918
|
-
" }",
|
|
919
|
-
"}",
|
|
920
|
-
"console.log(JSON.stringify({ url, title, text, scrollY }));"
|
|
921
|
-
].join("\n");
|
|
922
|
-
const result = await desktop.commands.run(`node --input-type=module -e ${shellSingleQuote(script)}`, {
|
|
923
|
-
requestTimeoutMs,
|
|
924
|
-
timeoutMs: 5_000
|
|
925
|
-
});
|
|
895
|
+
const result = await desktop.commands.run(chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "active", mode: "state" }), { requestTimeoutMs, timeoutMs: 5_000 });
|
|
926
896
|
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
927
|
-
return {};
|
|
928
|
-
}
|
|
929
|
-
try {
|
|
930
|
-
const parsed = JSON.parse((result.stdout ?? "{}").trim() || "{}");
|
|
931
|
-
if (!parsed || typeof parsed !== "object") {
|
|
932
|
-
return {};
|
|
933
|
-
}
|
|
934
|
-
const record = parsed;
|
|
935
|
-
return {
|
|
936
|
-
...(typeof record.url === "string" && record.url.length > 0 ? { url: record.url } : {}),
|
|
937
|
-
...(typeof record.title === "string" && record.title.length > 0 ? { title: record.title } : {}),
|
|
938
|
-
...(typeof record.text === "string" && record.text.length > 0 ? { text: record.text } : {}),
|
|
939
|
-
...(typeof record.scrollY === "number" && Number.isFinite(record.scrollY) ? { scrollY: record.scrollY } : {})
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
catch {
|
|
943
|
-
return {};
|
|
897
|
+
return unavailable(`probe exited ${result.exitCode}: ${tailOf(result.stderr ?? result.stdout ?? "")}`);
|
|
944
898
|
}
|
|
899
|
+
const parsed = parseChromeCdpProbeOutput(result.stdout);
|
|
900
|
+
if (parsed.unavailable !== undefined)
|
|
901
|
+
return unavailable(parsed.unavailable);
|
|
902
|
+
return {
|
|
903
|
+
...(parsed.url === undefined ? {} : { url: parsed.url }),
|
|
904
|
+
...(parsed.title === undefined ? {} : { title: parsed.title }),
|
|
905
|
+
...(parsed.text === undefined ? {} : { text: parsed.text }),
|
|
906
|
+
...(parsed.scrollY === undefined ? {} : { scrollY: parsed.scrollY })
|
|
907
|
+
};
|
|
945
908
|
};
|
|
946
909
|
}
|
|
947
910
|
/**
|
|
948
911
|
* Read the running browser's actual outer-window bounds and CSS layout viewport through the
|
|
949
912
|
* already-enabled local Chrome DevTools endpoint. The returned values come from `window.*` in
|
|
950
913
|
* the target page; requested E2B resolution is deliberately not an input to this function.
|
|
914
|
+
* `undefined` carries the reason the measurement is missing via `onUnavailable`, so the geometry
|
|
915
|
+
* warning can name the cause (a dead CDP endpoint, no python3) instead of only the symptom.
|
|
951
916
|
*/
|
|
952
|
-
export function makeChromeDesktopGeometryObserver(desktop, requestTimeoutMs, endpoint, targetId) {
|
|
917
|
+
export function makeChromeDesktopGeometryObserver(desktop, requestTimeoutMs, endpoint, targetId, onUnavailable) {
|
|
953
918
|
return async () => {
|
|
954
|
-
const
|
|
955
|
-
...chromeCdpPageSelectionScript(endpoint, targetId),
|
|
956
|
-
"if (!page || typeof WebSocket !== 'function' || !page.webSocketDebuggerUrl) { console.log('{}'); process.exit(0); }",
|
|
957
|
-
"const ws = new WebSocket(page.webSocketDebuggerUrl);",
|
|
958
|
-
"const result = await new Promise((resolve) => {",
|
|
959
|
-
" const timer = setTimeout(() => resolve(undefined), 1500);",
|
|
960
|
-
" ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { returnByValue: true, expression: '({ browserWindow: { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }, viewport: { width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio } })' } }));",
|
|
961
|
-
" ws.onmessage = (event) => {",
|
|
962
|
-
" try {",
|
|
963
|
-
" const payload = JSON.parse(String(event.data));",
|
|
964
|
-
" if (payload.id !== 1) return;",
|
|
965
|
-
" clearTimeout(timer);",
|
|
966
|
-
" resolve(payload.result && payload.result.result && payload.result.result.value);",
|
|
967
|
-
" } catch { clearTimeout(timer); resolve(undefined); }",
|
|
968
|
-
" };",
|
|
969
|
-
" ws.onerror = () => { clearTimeout(timer); resolve(undefined); };",
|
|
970
|
-
"}).finally(() => { try { ws.close(); } catch {} });",
|
|
971
|
-
"console.log(JSON.stringify(result ? { ...result, targetId: String(page.id || '') } : {}));"
|
|
972
|
-
].join("\n");
|
|
973
|
-
const result = await desktop.commands.run(`node --input-type=module -e ${shellSingleQuote(script)}`, {
|
|
974
|
-
requestTimeoutMs,
|
|
975
|
-
timeoutMs: 5_000
|
|
976
|
-
});
|
|
919
|
+
const result = await desktop.commands.run(chromeCdpProbeCommand({ ...endpoint, ...(targetId === undefined ? {} : { targetId }), prefer: "pinned", mode: "geometry" }), { requestTimeoutMs, timeoutMs: 5_000 });
|
|
977
920
|
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
921
|
+
onUnavailable?.(`probe exited ${result.exitCode}: ${tailOf(result.stderr ?? result.stdout ?? "")}`);
|
|
978
922
|
return undefined;
|
|
979
923
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const record = parsed;
|
|
985
|
-
const rawWindow = record.browserWindow;
|
|
986
|
-
const rawViewport = record.viewport;
|
|
987
|
-
if (!isMeasuredRect(rawWindow) || !isMeasuredViewport(rawViewport))
|
|
988
|
-
return undefined;
|
|
989
|
-
return {
|
|
990
|
-
browserWindow: { ...rawWindow, source: "cdp" },
|
|
991
|
-
viewport: { ...rawViewport, source: "cdp" },
|
|
992
|
-
...(typeof record.targetId === "string" && record.targetId.length > 0 ? { targetId: record.targetId } : {})
|
|
993
|
-
};
|
|
924
|
+
const parsed = parseChromeCdpProbeOutput(result.stdout);
|
|
925
|
+
if (parsed.unavailable !== undefined) {
|
|
926
|
+
onUnavailable?.(parsed.unavailable);
|
|
927
|
+
return undefined;
|
|
994
928
|
}
|
|
995
|
-
|
|
929
|
+
if (!isMeasuredRect(parsed.browserWindow) || !isMeasuredViewport(parsed.viewport)) {
|
|
930
|
+
onUnavailable?.("the page reported no usable window or viewport dimensions");
|
|
996
931
|
return undefined;
|
|
997
932
|
}
|
|
933
|
+
return {
|
|
934
|
+
browserWindow: { ...parsed.browserWindow, source: "cdp" },
|
|
935
|
+
viewport: { ...parsed.viewport, source: "cdp" },
|
|
936
|
+
...(parsed.targetId === undefined ? {} : { targetId: parsed.targetId })
|
|
937
|
+
};
|
|
998
938
|
};
|
|
999
939
|
}
|
|
1000
940
|
function isMeasuredRect(value) {
|
|
@@ -1063,12 +1003,18 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1063
1003
|
else {
|
|
1064
1004
|
warnings.push(`Browser window bounds could not be measured for lane ${args.laneId}; the live stream will use the full desktop.`);
|
|
1065
1005
|
}
|
|
1006
|
+
let cdpUnavailable;
|
|
1066
1007
|
const chromeGeometry = args.browserFamily === "chromium"
|
|
1067
1008
|
? await makeChromeDesktopGeometryObserver(args.desktop, args.requestTimeoutMs, {
|
|
1068
1009
|
...(args.launchIdentity?.cdpPort === undefined ? {} : { cdpPort: args.launchIdentity.cdpPort }),
|
|
1069
1010
|
...(args.launchIdentity?.profileDir === undefined ? {} : { profileDir: args.launchIdentity.profileDir }),
|
|
1070
1011
|
targetUrl: args.targetUrl
|
|
1071
|
-
}, args.browserTargetId
|
|
1012
|
+
}, args.browserTargetId, (reason) => {
|
|
1013
|
+
cdpUnavailable = reason;
|
|
1014
|
+
})().catch((error) => {
|
|
1015
|
+
cdpUnavailable = toErrorMessage(error);
|
|
1016
|
+
return undefined;
|
|
1017
|
+
})
|
|
1072
1018
|
: undefined;
|
|
1073
1019
|
const browserWindow = chromeGeometry?.browserWindow ?? xdotoolWindow;
|
|
1074
1020
|
const viewport = chromeGeometry?.viewport;
|
|
@@ -1079,9 +1025,12 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1079
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}.`);
|
|
1080
1026
|
}
|
|
1081
1027
|
if (!viewport) {
|
|
1028
|
+
// Name the cause, not only the symptom: the same dead DevTools channel that loses the viewport
|
|
1029
|
+
// loses every url/text observation, and a reader of the bundle should learn that here (#514).
|
|
1030
|
+
const cause = cdpUnavailable === undefined ? "" : ` DevTools probe: ${redactText(cdpUnavailable)}.`;
|
|
1082
1031
|
warnings.push(args.browserFamily === "firefox"
|
|
1083
1032
|
? `Browser CSS viewport measurement is unavailable for Firefox on lane ${args.laneId}; stream.viewport is omitted instead of reading a different browser's CDP endpoint.`
|
|
1084
|
-
: `Browser CSS viewport could not be measured for lane ${args.laneId}; stream.viewport is omitted instead of copying the requested screen resolution
|
|
1033
|
+
: `Browser CSS viewport could not be measured for lane ${args.laneId}; stream.viewport is omitted instead of copying the requested screen resolution.${cause}`);
|
|
1085
1034
|
}
|
|
1086
1035
|
return {
|
|
1087
1036
|
...(browserWindowId === undefined ? {} : { browserWindowId }),
|
|
@@ -1752,7 +1701,13 @@ export async function runCuaLane(spec, deps) {
|
|
|
1752
1701
|
...(browserLaunchIdentity?.cdpPort === undefined ? {} : { cdpPort: browserLaunchIdentity.cdpPort }),
|
|
1753
1702
|
...(browserLaunchIdentity?.profileDir === undefined ? {} : { profileDir: browserLaunchIdentity.profileDir }),
|
|
1754
1703
|
targetUrl
|
|
1755
|
-
}, browserTargetId
|
|
1704
|
+
}, browserTargetId,
|
|
1705
|
+
// Once per lane: a dark observation channel is a gap in the instrument, and the
|
|
1706
|
+
// funnel's NEVER MEASURED count needs this line to explain itself (#514).
|
|
1707
|
+
(reason) => {
|
|
1708
|
+
warnings.push(`Browser-state observer unavailable for lane ${spec.laneId} (${redactText(deps.scrubKnownValues(reason))}); ` +
|
|
1709
|
+
"urlIncludes/urlPathEquals/textIncludes stop conditions and task criteria are NOT being measured this session.");
|
|
1710
|
+
})
|
|
1756
1711
|
}
|
|
1757
1712
|
}
|
|
1758
1713
|
: {}),
|
|
@@ -3288,13 +3243,17 @@ async function runSubjectServePipeline(desktop, args) {
|
|
|
3288
3243
|
if (needsNodeRuntime(serveCommands)) {
|
|
3289
3244
|
const runtimeStartedAt = now();
|
|
3290
3245
|
emitPhaseStarted(args.onPhase, now, "runtime", "providing the Node runtime the serve pipeline needs");
|
|
3291
|
-
const bootstrap = await
|
|
3246
|
+
const bootstrap = await runProvisioningStepWithOneRetry(desktop, {
|
|
3292
3247
|
name: "subject-runtime-node",
|
|
3293
3248
|
command: nodeBootstrapCommand(),
|
|
3294
3249
|
cwd: SUBJECT_DIR,
|
|
3295
3250
|
timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
3296
3251
|
requestTimeoutMs: args.requestTimeoutMs,
|
|
3297
|
-
|
|
3252
|
+
timers,
|
|
3253
|
+
retryPhase: "runtime-retry",
|
|
3254
|
+
retryMessage: "Node runtime bootstrap",
|
|
3255
|
+
onPhase: args.onPhase,
|
|
3256
|
+
now
|
|
3298
3257
|
});
|
|
3299
3258
|
let ok = bootstrap.ok;
|
|
3300
3259
|
const corepack = ok ? corepackCommandFor(serveCommands) : undefined;
|
|
@@ -3311,23 +3270,39 @@ async function runSubjectServePipeline(desktop, args) {
|
|
|
3311
3270
|
}
|
|
3312
3271
|
emitPhaseCompleted(args.onPhase, now, runtimeStartedAt, "runtime", ok, ok ? "Node runtime ready" : "could not provide a Node runtime");
|
|
3313
3272
|
if (!ok) {
|
|
3314
|
-
throw new Error(`the subject's serve pipeline needs a Node runtime and this desktop template has none, and bootstrapping one failed: ${tailOf(args.scrub(bootstrap.logTail))}. Use execution.desktop.template with an image that ships Node, or change serve.install to a runtime the template provides.`);
|
|
3273
|
+
throw new Error(`the subject's serve pipeline needs a Node runtime and this desktop template has none, and bootstrapping one failed${bootstrap.attempts === 2 ? " twice" : ""}: ${tailOf(args.scrub(bootstrap.logTail))}. Use execution.desktop.template with an image that ships Node, or change serve.install to a runtime the template provides.`);
|
|
3315
3274
|
}
|
|
3316
3275
|
}
|
|
3317
3276
|
if (args.serve.install) {
|
|
3318
3277
|
const installStartedAt = now();
|
|
3319
3278
|
emitPhaseStarted(args.onPhase, now, "install", "installing subject dependencies");
|
|
3320
|
-
const install = await
|
|
3279
|
+
const install = await runProvisioningStepWithOneRetry(desktop, {
|
|
3321
3280
|
name: "subject-install",
|
|
3322
3281
|
command: args.serve.install,
|
|
3323
3282
|
cwd: SUBJECT_DIR,
|
|
3324
3283
|
timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
3325
3284
|
requestTimeoutMs: args.requestTimeoutMs,
|
|
3326
|
-
|
|
3285
|
+
timers,
|
|
3286
|
+
retryPhase: "install-retry",
|
|
3287
|
+
retryMessage: "subject install",
|
|
3288
|
+
onPhase: args.onPhase,
|
|
3289
|
+
now
|
|
3327
3290
|
});
|
|
3328
|
-
emitPhaseCompleted(args.onPhase, now, installStartedAt, "install", install.ok, install.ok
|
|
3291
|
+
emitPhaseCompleted(args.onPhase, now, installStartedAt, "install", install.ok, install.ok
|
|
3292
|
+
? install.attempts === 2
|
|
3293
|
+
? "subject dependencies installed (on the second attempt)"
|
|
3294
|
+
: "subject dependencies installed"
|
|
3295
|
+
: install.attempts === 2
|
|
3296
|
+
? "subject install failed twice"
|
|
3297
|
+
: "subject install failed");
|
|
3329
3298
|
if (!install.ok) {
|
|
3330
|
-
|
|
3299
|
+
// Lead with the line a person can act on; npm's own trace follows it (#602).
|
|
3300
|
+
const headline = install.timedOut
|
|
3301
|
+
? `subject install timed out after ${args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS}ms`
|
|
3302
|
+
: install.attempts === 2
|
|
3303
|
+
? `subject install failed twice (exit ${install.firstExitCode ?? "null"}, then exit ${install.exitCode ?? "null"}); the sandbox could not complete serve.install`
|
|
3304
|
+
: `subject install failed (exit ${install.exitCode ?? "null"})`;
|
|
3305
|
+
throw new Error(`${headline}: ${tailOf(args.scrub(install.logTail))}`);
|
|
3331
3306
|
}
|
|
3332
3307
|
await refresh();
|
|
3333
3308
|
}
|