@foldrun/core 2.8.0 → 2.9.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/src/issues.js +4 -1
- package/dist/src/run-container.d.ts +6 -3
- package/dist/src/run-container.js +5 -2
- package/dist/src/runner.d.ts +5 -0
- package/dist/src/runner.js +57 -1
- package/package.json +1 -1
- package/src/issues.ts +4 -1
- package/src/run-container.ts +5 -2
- package/src/runner.ts +58 -1
package/dist/src/issues.js
CHANGED
|
@@ -41,7 +41,10 @@ const PLATFORM_CAUSES = [
|
|
|
41
41
|
key: "model-cap",
|
|
42
42
|
label: "The model usage cap is reached",
|
|
43
43
|
fix: "runs resume when the cap resets; nothing to change in the desks.",
|
|
44
|
-
|
|
44
|
+
// Also the runner's own sentence for a spent subscription window
|
|
45
|
+
// (refusal.ts explainRefusal): on 23 Oct 2026 "the 7-day window is 100%
|
|
46
|
+
// used" filed five desks as their own failures.
|
|
47
|
+
test: /weekly (usage )?limit|usage limit (reached|exceeded)|reached your (weekly|usage) limit|window is (100% used|spent)/i,
|
|
45
48
|
},
|
|
46
49
|
{
|
|
47
50
|
// The provider out of capacity is not the account's cap: it passes in
|
|
@@ -328,9 +328,12 @@ export declare const RETRY_SH = "retry() { what=$1; shift; n=1; until \"$@\"; do
|
|
|
328
328
|
* and make one browse call per engine on the full image:
|
|
329
329
|
* docker buildx imagetools inspect node:22-trixie-slim --format '{{json .Manifest}}' | jq -r .digest
|
|
330
330
|
* The digest names the multi-arch index, so the one pin serves amd64 and
|
|
331
|
-
* arm64 alike.
|
|
332
|
-
|
|
333
|
-
export declare const DOCKERFILE = "FROM node:22-trixie-slim@sha256:b26b04c123d9ff8ab646ceb18b9d75a1173acf64b9a401094b906d27b29338d4 AS base\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends python3 python3-venv ca-certificates bash util-linux tar openssh-client sshpass git curl \\\n && rm -rf /var/lib/apt/lists/* \\\n && useradd -m -u 10001 agent\n# uv builds the declared python environments (runtime.ts): a fraction of\n# pip's time, and it can fetch a pinned python the image does not carry.\nCOPY --from=ghcr.io/astral-sh/uv:0.11.25 /uv /uvx /usr/local/bin/\nENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules\n\nFROM base AS core\nWORKDIR /opt/runner\nCOPY foldrun-core.tgz ./\nRUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\\n && npm cache clean --force >/dev/null 2>&1 \\\n && rm -f foldrun-core.tgz node_modules/@foldrun/core/README.md\n\nFROM base AS slim\n# Fonts, because a step that draws text without a browser still needs them:\n# the full image gets 51 from Playwright's --with-deps, slim had none, so from\n# 2026-09-29 (when non-browsing steps moved to slim) every resvg/sharp diagram\n# rendered its labels as empty boxes \u2014 expert-witness-desk's validator failed\n# four passes on a post image that was \"three empty boxes\". DejaVu covers\n# Latin, Greek and Cyrillic in a few MB; fontconfig is how renderers find it.\n# DejaVu alone still drew emoji, Chinese, Japanese, Korean, Devanagari and\n# Thai as empty boxes (reproduced 2026-10-01 with resvg-js 2.6 and sharp\n# 0.34), so four more, the smallest set that drew all of them \u2014 8 MB:\n# Symbola emoji and symbols, as outlines. NOT Noto Color Emoji (10 MB):\n# resvg draws nothing from its bitmap glyphs, so a resvg\n# diagram kept its empty boxes.\n# WenQuanYi Micro Hei Chinese, Japanese kana and Korean Hangul, one file.\n# Lohit Devanagari, Loma Hindi and Thai.\n# Here, not in base, so the full image's browser layers stay cached.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends fonts-dejavu-core fontconfig fonts-symbola fonts-wqy-microhei fonts-lohit-deva fonts-tlwg-loma-otf \\\n && rm -rf /var/lib/apt/lists/* \\\n && fc-cache -f >/dev/null\n# The Playwright client only, the same pinned version as the browser pod's\n# server (the wire protocol is version-locked), so a tool that imports it\n# still loads; there is nothing here for it to launch.\n# npm's download cache is dropped in the same layer: it was 190 MB of every\n# image, read by nothing after the build.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null && npm cache clean --force >/dev/null 2>&1\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n\nFROM base AS browsers\n# Real browsers, because directories and portals increasingly render with\n# JavaScript and a plain fetch sees only the empty shell. Chromium is the one the\n# browser tool drives by default; Firefox and WebKit (Safari's engine) are\n# there for an agent asked to see a page the way those render it. Installed\n# to a fixed path the agent user can read (the default cache would be\n# root's HOME). The browsers' own sandboxes are disabled at launch time \u2014 in\n# this platform the container/gVisor IS the sandbox, and the two fight.\n# Pinned exactly, not @1: web browse's numbered elements use ariaSnapshot's\n# \"ai\" mode (1.59+), and the per-account browser pod runs this same image, so\n# a floating version would move both under a working tool on any rebuild.\n# Raise it on purpose, with the gallery tests and one real call.\n# axe-core beside it is web browse's mode=a11y: one file the tool injects\n# into the page; without it the mode falls back to its own shorter checks.\n# Xvfb by name: the browser pod starts it at boot and web browse starts one\n# per call for headless: false. It used to arrive only as one of Playwright's\n# --with-deps packages, so a Playwright bump that dropped it would have\n# broken every windowed browser without a word. No xauth: nothing here runs\n# xvfb-run, and Xvfb started without -auth takes the local client as it is.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends xvfb \\\n && rm -rf /var/lib/apt/lists/*\n# Google Chrome and Chrome beta come from Google's apt repository, which\n# Playwright adds; each is tried ENGINE_DOWNLOAD_ATTEMPTS times. Only on\n# x86_64: Google ships no Linux Chrome for arm64, so there it is not tried.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null \\\n && playwright install --with-deps chromium firefox webkit >/dev/null \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && if [ \"$(uname -m)\" = x86_64 ]; then \\\n retry chrome playwright install --with-deps chrome >/dev/null || echo \"runner image: chrome not installed - engine: chrome would fall back to chromium\" >&2; \\\n retry chrome-beta playwright install --with-deps chrome-beta >/dev/null || echo \"runner image: chrome-beta not installed\" >&2; \\\n else echo \"runner image: no Google Chrome for $(uname -m) - engine: chrome falls back to chromium\"; fi \\\n && chmod -R a+rX /opt/browser \\\n && npm cache clean --force >/dev/null 2>&1\n# Lightpanda (engine: lightpanda): a browser that runs the JavaScript and\n# never draws \u2014 a fraction of Chromium's memory for reading and filling\n# pages. Its own program, which web browse starts beside a call and drives\n# over the DevTools protocol; shipped unmodified (AGPL-3.0). Pinned by\n# checksum (GitHub's own asset digests, checked again here). 0.4.1 since the\n# move to Debian 13: 0.3.7 on wants glibc 2.38, and Debian 12's 2.36 held it\n# at 0.3.6. An arch without a build leaves the engine out, and the tool says\n# so when asked for it. Each download (with its checksum) is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times before the engine is left out.\n# Raise it on purpose: the new release's two sha256s, then one\n# engine=lightpanda call on each arch.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1d40801e72c0bc61b2cbd3f3562bcfc46de7b79e0568f33f686b64f2e587610a ;; \\\n aarch64) sum=664775c7f5ab69cc3189954c7f9345e25c167cb4dace016173e629f9a5e82c42 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/lightpanda \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /opt/browser/lightpanda/lightpanda \"https://github.com/lightpanda-io/browser/releases/download/0.4.1/lightpanda-$arch-linux\" \\\n && echo \"$sum /opt/browser/lightpanda/lightpanda\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry lightpanda fetch; then \\\n chmod a+rx /opt/browser/lightpanda/lightpanda && /opt/browser/lightpanda/lightpanda version; \\\n else rm -f /opt/browser/lightpanda/lightpanda; echo \"runner image: lightpanda not installed for $arch - engine: lightpanda says so when asked\" >&2; fi\n# Obscura (engine: obscura): a headless browser in Rust around V8 that does\n# draw \u2014 screenshots, a raster PDF, a screencast \u2014 light like Lightpanda.\n# Apache-2.0. The plain build, not -stealth: this platform does not help a\n# page not see automation. Two binaries, obscura and obscura-worker, which\n# must sit side by side. Pinned by checksum; glibc 2.35 is its floor, so\n# Debian 13 (2.41) runs it. As above, a download is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times, then a failed one leaves the engine out.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1534d1e6ddaf3d080ec4091eb41d0a4d8cc042a48b607d3c410fc13b482a9eec ;; \\\n aarch64) sum=5ecf980bca3060236a7a86ec7ed83d943e6598ee87caa46d20325d90bc75f979 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/obscura \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /tmp/obscura.tgz \"https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.3/obscura-$arch-linux.tar.gz\" \\\n && echo \"$sum /tmp/obscura.tgz\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry obscura fetch \\\n && tar -xzf /tmp/obscura.tgz -C /opt/browser/obscura obscura obscura-worker; then \\\n chmod a+rx /opt/browser/obscura/obscura /opt/browser/obscura/obscura-worker && /opt/browser/obscura/obscura --version; \\\n else rm -rf /opt/browser/obscura/*; echo \"runner image: obscura not installed for $arch - engine: obscura says so when asked\" >&2; fi \\\n && rm -f /tmp/obscura.tgz\n# What this image actually has, written down: /opt/browser/engines.json, one\n# entry per engine (its path and version, or null when the install above\n# left it out) plus Xvfb. Chromium's version is its own --version (dotted,\n# so FOLDRUN_RUNNER_ENGINES carries it: a pinned user_agent on engine\n# chromium is compared with it, not with Google Chrome's) then the build.\n# web browse reads it for its \"built without\" answer, and the deploy reads\n# it to report the engines beside the version.\n# On x86_64 a missing engine FAILS the build: every engine ships for that\n# arch, so a gap there is a failed download, and an image that quietly lacks\n# the Chrome or Lightpanda a desk names would fail those runs instead. A\n# failed build leaves the previous image running and says why. Elsewhere\n# (arm64 has no Google Chrome) the gap is recorded and the image ships.\nRUN node -e ' \\\n const fs = require(\"fs\"), cp = require(\"child_process\"), pw = require(\"playwright\"); \\\n const at = (p) => (p && fs.existsSync(p) ? p : null); \\\n const ver = (bin, args) => { try { return cp.execFileSync(bin, args, { timeout: 30000, stdio: [\"ignore\", \"pipe\", \"ignore\"] }).toString().trim().split(\"\\n\")[0] || null; } catch { return null; } }; \\\n const one = (p, v) => (p ? { path: p, version: v } : null); \\\n const pwBuild = (p) => (p ? (p.match(/(chromium|firefox|webkit)-(\\d+)/) || [])[0] || null : null); \\\n const lp = at(\"/opt/browser/lightpanda/lightpanda\"); \\\n const cr = at(pw.chromium.executablePath()); \\\n const ob = at(\"/opt/browser/obscura/obscura-worker\") && at(\"/opt/browser/obscura/obscura\"); \\\n const engines = { \\\n chromium: one(cr, cr && [ver(cr, [\"--version\"]), pwBuild(cr)].filter(Boolean).join(\" \")), \\\n chrome: one(at(\"/opt/google/chrome/chrome\"), ver(\"/opt/google/chrome/chrome\", [\"--version\"])), \\\n \"chrome-beta\": one(at(\"/opt/google/chrome-beta/chrome\"), ver(\"/opt/google/chrome-beta/chrome\", [\"--version\"])), \\\n firefox: one(at(pw.firefox.executablePath()), pwBuild(pw.firefox.executablePath())), \\\n webkit: one(at(pw.webkit.executablePath()), pwBuild(pw.webkit.executablePath())), \\\n lightpanda: one(lp, lp && ver(lp, [\"version\"])), \\\n obscura: one(ob, ob && ver(ob, [\"--version\"])), \\\n }; \\\n const xvfb = ver(\"sh\", [\"-c\", \"command -v Xvfb\"]); \\\n const arch = cp.execFileSync(\"uname\", [\"-m\"]).toString().trim(); \\\n const manifest = { arch, playwright: require(\"playwright/package.json\").version, engines, xvfb }; \\\n fs.writeFileSync(\"/opt/browser/engines.json\", JSON.stringify(manifest, null, 2) + \"\\n\"); \\\n console.log(JSON.stringify(manifest)); \\\n const missing = [\"chromium\", \"chrome\", \"firefox\", \"webkit\", \"lightpanda\", \"obscura\"].filter((e) => !engines[e]).concat(xvfb ? [] : [\"Xvfb\"]); \\\n if (missing.length && arch === \"x86_64\") { \\\n console.error(\"runner image: MISSING ENGINES on x86_64: \" + missing.join(\", \")); \\\n console.error(\"runner image: every engine ships for x86_64, so a missing one is a failed download (the retry lines above say which) - failing the build\"); \\\n process.exit(1); \\\n } \\\n if (missing.length) console.error(\"runner image: built without \" + missing.join(\", \") + \" on \" + arch); \\\n ' \\\n && chmod a+r /opt/browser/engines.json\n\nFROM browsers AS full\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n";
|
|
331
|
+
* arm64 alike.
|
|
332
|
+
*
|
|
333
|
+
* 23 Oct 2026: Node 22.23.4 (the 21 Oct security release: Brotli decoder
|
|
334
|
+
* RCE, TLS hostname bypasses); the pin before carried 22.23.3. */
|
|
335
|
+
export declare const NODE_BASE = "node:22-trixie-slim@sha256:8eb619e402eaede1748a3c8173bffab391592812e85136452f781741c7242ddf";
|
|
336
|
+
export declare const DOCKERFILE = "FROM node:22-trixie-slim@sha256:8eb619e402eaede1748a3c8173bffab391592812e85136452f781741c7242ddf AS base\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends python3 python3-venv ca-certificates bash util-linux tar openssh-client sshpass git curl \\\n && rm -rf /var/lib/apt/lists/* \\\n && useradd -m -u 10001 agent\n# uv builds the declared python environments (runtime.ts): a fraction of\n# pip's time, and it can fetch a pinned python the image does not carry.\nCOPY --from=ghcr.io/astral-sh/uv:0.11.25 /uv /uvx /usr/local/bin/\nENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules\n\nFROM base AS core\nWORKDIR /opt/runner\nCOPY foldrun-core.tgz ./\nRUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\\n && npm cache clean --force >/dev/null 2>&1 \\\n && rm -f foldrun-core.tgz node_modules/@foldrun/core/README.md\n\nFROM base AS slim\n# Fonts, because a step that draws text without a browser still needs them:\n# the full image gets 51 from Playwright's --with-deps, slim had none, so from\n# 2026-09-29 (when non-browsing steps moved to slim) every resvg/sharp diagram\n# rendered its labels as empty boxes \u2014 expert-witness-desk's validator failed\n# four passes on a post image that was \"three empty boxes\". DejaVu covers\n# Latin, Greek and Cyrillic in a few MB; fontconfig is how renderers find it.\n# DejaVu alone still drew emoji, Chinese, Japanese, Korean, Devanagari and\n# Thai as empty boxes (reproduced 2026-10-01 with resvg-js 2.6 and sharp\n# 0.34), so four more, the smallest set that drew all of them \u2014 8 MB:\n# Symbola emoji and symbols, as outlines. NOT Noto Color Emoji (10 MB):\n# resvg draws nothing from its bitmap glyphs, so a resvg\n# diagram kept its empty boxes.\n# WenQuanYi Micro Hei Chinese, Japanese kana and Korean Hangul, one file.\n# Lohit Devanagari, Loma Hindi and Thai.\n# Here, not in base, so the full image's browser layers stay cached.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends fonts-dejavu-core fontconfig fonts-symbola fonts-wqy-microhei fonts-lohit-deva fonts-tlwg-loma-otf \\\n && rm -rf /var/lib/apt/lists/* \\\n && fc-cache -f >/dev/null\n# The Playwright client only, the same pinned version as the browser pod's\n# server (the wire protocol is version-locked), so a tool that imports it\n# still loads; there is nothing here for it to launch.\n# npm's download cache is dropped in the same layer: it was 190 MB of every\n# image, read by nothing after the build.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null && npm cache clean --force >/dev/null 2>&1\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n\nFROM base AS browsers\n# Real browsers, because directories and portals increasingly render with\n# JavaScript and a plain fetch sees only the empty shell. Chromium is the one the\n# browser tool drives by default; Firefox and WebKit (Safari's engine) are\n# there for an agent asked to see a page the way those render it. Installed\n# to a fixed path the agent user can read (the default cache would be\n# root's HOME). The browsers' own sandboxes are disabled at launch time \u2014 in\n# this platform the container/gVisor IS the sandbox, and the two fight.\n# Pinned exactly, not @1: web browse's numbered elements use ariaSnapshot's\n# \"ai\" mode (1.59+), and the per-account browser pod runs this same image, so\n# a floating version would move both under a working tool on any rebuild.\n# Raise it on purpose, with the gallery tests and one real call.\n# axe-core beside it is web browse's mode=a11y: one file the tool injects\n# into the page; without it the mode falls back to its own shorter checks.\n# Xvfb by name: the browser pod starts it at boot and web browse starts one\n# per call for headless: false. It used to arrive only as one of Playwright's\n# --with-deps packages, so a Playwright bump that dropped it would have\n# broken every windowed browser without a word. No xauth: nothing here runs\n# xvfb-run, and Xvfb started without -auth takes the local client as it is.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends xvfb \\\n && rm -rf /var/lib/apt/lists/*\n# Google Chrome and Chrome beta come from Google's apt repository, which\n# Playwright adds; each is tried ENGINE_DOWNLOAD_ATTEMPTS times. Only on\n# x86_64: Google ships no Linux Chrome for arm64, so there it is not tried.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null \\\n && playwright install --with-deps chromium firefox webkit >/dev/null \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && if [ \"$(uname -m)\" = x86_64 ]; then \\\n retry chrome playwright install --with-deps chrome >/dev/null || echo \"runner image: chrome not installed - engine: chrome would fall back to chromium\" >&2; \\\n retry chrome-beta playwright install --with-deps chrome-beta >/dev/null || echo \"runner image: chrome-beta not installed\" >&2; \\\n else echo \"runner image: no Google Chrome for $(uname -m) - engine: chrome falls back to chromium\"; fi \\\n && chmod -R a+rX /opt/browser \\\n && npm cache clean --force >/dev/null 2>&1\n# Lightpanda (engine: lightpanda): a browser that runs the JavaScript and\n# never draws \u2014 a fraction of Chromium's memory for reading and filling\n# pages. Its own program, which web browse starts beside a call and drives\n# over the DevTools protocol; shipped unmodified (AGPL-3.0). Pinned by\n# checksum (GitHub's own asset digests, checked again here). 0.4.1 since the\n# move to Debian 13: 0.3.7 on wants glibc 2.38, and Debian 12's 2.36 held it\n# at 0.3.6. An arch without a build leaves the engine out, and the tool says\n# so when asked for it. Each download (with its checksum) is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times before the engine is left out.\n# Raise it on purpose: the new release's two sha256s, then one\n# engine=lightpanda call on each arch.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1d40801e72c0bc61b2cbd3f3562bcfc46de7b79e0568f33f686b64f2e587610a ;; \\\n aarch64) sum=664775c7f5ab69cc3189954c7f9345e25c167cb4dace016173e629f9a5e82c42 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/lightpanda \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /opt/browser/lightpanda/lightpanda \"https://github.com/lightpanda-io/browser/releases/download/0.4.1/lightpanda-$arch-linux\" \\\n && echo \"$sum /opt/browser/lightpanda/lightpanda\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry lightpanda fetch; then \\\n chmod a+rx /opt/browser/lightpanda/lightpanda && /opt/browser/lightpanda/lightpanda version; \\\n else rm -f /opt/browser/lightpanda/lightpanda; echo \"runner image: lightpanda not installed for $arch - engine: lightpanda says so when asked\" >&2; fi\n# Obscura (engine: obscura): a headless browser in Rust around V8 that does\n# draw \u2014 screenshots, a raster PDF, a screencast \u2014 light like Lightpanda.\n# Apache-2.0. The plain build, not -stealth: this platform does not help a\n# page not see automation. Two binaries, obscura and obscura-worker, which\n# must sit side by side. Pinned by checksum; glibc 2.35 is its floor, so\n# Debian 13 (2.41) runs it. As above, a download is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times, then a failed one leaves the engine out.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1534d1e6ddaf3d080ec4091eb41d0a4d8cc042a48b607d3c410fc13b482a9eec ;; \\\n aarch64) sum=5ecf980bca3060236a7a86ec7ed83d943e6598ee87caa46d20325d90bc75f979 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/obscura \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /tmp/obscura.tgz \"https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.3/obscura-$arch-linux.tar.gz\" \\\n && echo \"$sum /tmp/obscura.tgz\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry obscura fetch \\\n && tar -xzf /tmp/obscura.tgz -C /opt/browser/obscura obscura obscura-worker; then \\\n chmod a+rx /opt/browser/obscura/obscura /opt/browser/obscura/obscura-worker && /opt/browser/obscura/obscura --version; \\\n else rm -rf /opt/browser/obscura/*; echo \"runner image: obscura not installed for $arch - engine: obscura says so when asked\" >&2; fi \\\n && rm -f /tmp/obscura.tgz\n# What this image actually has, written down: /opt/browser/engines.json, one\n# entry per engine (its path and version, or null when the install above\n# left it out) plus Xvfb. Chromium's version is its own --version (dotted,\n# so FOLDRUN_RUNNER_ENGINES carries it: a pinned user_agent on engine\n# chromium is compared with it, not with Google Chrome's) then the build.\n# web browse reads it for its \"built without\" answer, and the deploy reads\n# it to report the engines beside the version.\n# On x86_64 a missing engine FAILS the build: every engine ships for that\n# arch, so a gap there is a failed download, and an image that quietly lacks\n# the Chrome or Lightpanda a desk names would fail those runs instead. A\n# failed build leaves the previous image running and says why. Elsewhere\n# (arm64 has no Google Chrome) the gap is recorded and the image ships.\nRUN node -e ' \\\n const fs = require(\"fs\"), cp = require(\"child_process\"), pw = require(\"playwright\"); \\\n const at = (p) => (p && fs.existsSync(p) ? p : null); \\\n const ver = (bin, args) => { try { return cp.execFileSync(bin, args, { timeout: 30000, stdio: [\"ignore\", \"pipe\", \"ignore\"] }).toString().trim().split(\"\\n\")[0] || null; } catch { return null; } }; \\\n const one = (p, v) => (p ? { path: p, version: v } : null); \\\n const pwBuild = (p) => (p ? (p.match(/(chromium|firefox|webkit)-(\\d+)/) || [])[0] || null : null); \\\n const lp = at(\"/opt/browser/lightpanda/lightpanda\"); \\\n const cr = at(pw.chromium.executablePath()); \\\n const ob = at(\"/opt/browser/obscura/obscura-worker\") && at(\"/opt/browser/obscura/obscura\"); \\\n const engines = { \\\n chromium: one(cr, cr && [ver(cr, [\"--version\"]), pwBuild(cr)].filter(Boolean).join(\" \")), \\\n chrome: one(at(\"/opt/google/chrome/chrome\"), ver(\"/opt/google/chrome/chrome\", [\"--version\"])), \\\n \"chrome-beta\": one(at(\"/opt/google/chrome-beta/chrome\"), ver(\"/opt/google/chrome-beta/chrome\", [\"--version\"])), \\\n firefox: one(at(pw.firefox.executablePath()), pwBuild(pw.firefox.executablePath())), \\\n webkit: one(at(pw.webkit.executablePath()), pwBuild(pw.webkit.executablePath())), \\\n lightpanda: one(lp, lp && ver(lp, [\"version\"])), \\\n obscura: one(ob, ob && ver(ob, [\"--version\"])), \\\n }; \\\n const xvfb = ver(\"sh\", [\"-c\", \"command -v Xvfb\"]); \\\n const arch = cp.execFileSync(\"uname\", [\"-m\"]).toString().trim(); \\\n const manifest = { arch, playwright: require(\"playwright/package.json\").version, engines, xvfb }; \\\n fs.writeFileSync(\"/opt/browser/engines.json\", JSON.stringify(manifest, null, 2) + \"\\n\"); \\\n console.log(JSON.stringify(manifest)); \\\n const missing = [\"chromium\", \"chrome\", \"firefox\", \"webkit\", \"lightpanda\", \"obscura\"].filter((e) => !engines[e]).concat(xvfb ? [] : [\"Xvfb\"]); \\\n if (missing.length && arch === \"x86_64\") { \\\n console.error(\"runner image: MISSING ENGINES on x86_64: \" + missing.join(\", \")); \\\n console.error(\"runner image: every engine ships for x86_64, so a missing one is a failed download (the retry lines above say which) - failing the build\"); \\\n process.exit(1); \\\n } \\\n if (missing.length) console.error(\"runner image: built without \" + missing.join(\", \") + \" on \" + arch); \\\n ' \\\n && chmod a+r /opt/browser/engines.json\n\nFROM browsers AS full\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n";
|
|
334
337
|
/** Which build target of the runner image (see DOCKERFILE). */
|
|
335
338
|
export type RunnerVariant = "full" | "slim";
|
|
336
339
|
export declare function runnerImageTag(variant?: RunnerVariant): string;
|
|
@@ -742,8 +742,11 @@ export const RETRY_SH = `retry() { what=$1; shift; n=1; until "$@"; do if [ "$n"
|
|
|
742
742
|
* and make one browse call per engine on the full image:
|
|
743
743
|
* docker buildx imagetools inspect node:22-trixie-slim --format '{{json .Manifest}}' | jq -r .digest
|
|
744
744
|
* The digest names the multi-arch index, so the one pin serves amd64 and
|
|
745
|
-
* arm64 alike.
|
|
746
|
-
|
|
745
|
+
* arm64 alike.
|
|
746
|
+
*
|
|
747
|
+
* 23 Oct 2026: Node 22.23.4 (the 21 Oct security release: Brotli decoder
|
|
748
|
+
* RCE, TLS hostname bypasses); the pin before carried 22.23.3. */
|
|
749
|
+
export const NODE_BASE = "node:22-trixie-slim@sha256:8eb619e402eaede1748a3c8173bffab391592812e85136452f781741c7242ddf";
|
|
747
750
|
// Two images from one file, as build targets.
|
|
748
751
|
//
|
|
749
752
|
// full (the default, the last stage) — everything, browsers included. The
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -494,6 +494,11 @@ export declare function actedThenFailedCheck(events: {
|
|
|
494
494
|
* happened to create it.
|
|
495
495
|
*/
|
|
496
496
|
export declare function copyTreeBytes(from: string, to: string): void;
|
|
497
|
+
/** Whether two agents of one workspace call the same model supply: each
|
|
498
|
+
* resolved the way agentContext does — its own provider: block, else the
|
|
499
|
+
* workspace's — and compared in a stable key order. The account key is
|
|
500
|
+
* shared by both, so two that resolve to no block share it (Codex review). */
|
|
501
|
+
export declare function sameSupply(workspaceDir: string, tenant: string, a: string, b: string): boolean;
|
|
497
502
|
/** A refusal of the credential itself, in any of the wordings Claude Code
|
|
498
503
|
* has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
|
|
499
504
|
* step started one second before the 09:07:52Z refresh) matched none of the
|
package/dist/src/runner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// flow, so there is a single execution path.
|
|
5
5
|
import { latestRevisionId } from "./history.js";
|
|
6
6
|
import { recordCallIo, redactionsFor } from "./capture.js";
|
|
7
|
-
import { AUTH_REFUSAL } from "./issues.js";
|
|
7
|
+
import { AUTH_REFUSAL, platformCause } from "./issues.js";
|
|
8
8
|
import { hookNames } from "./hooks.js";
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
import fs from "node:fs";
|
|
@@ -3510,6 +3510,34 @@ function platformFallbackEnv() {
|
|
|
3510
3510
|
*/
|
|
3511
3511
|
const CREDENTIAL_ROTATION_WAIT_MS = 90_000;
|
|
3512
3512
|
const CREDENTIAL_POLL_MS = 2_000;
|
|
3513
|
+
/** Platform causes a rescuer on the same model supply cannot get past. */
|
|
3514
|
+
const SAME_SUPPLY_WALLS = new Set(["model-cap", "model-auth"]);
|
|
3515
|
+
/** Whether two agents of one workspace call the same model supply: each
|
|
3516
|
+
* resolved the way agentContext does — its own provider: block, else the
|
|
3517
|
+
* workspace's — and compared in a stable key order. The account key is
|
|
3518
|
+
* shared by both, so two that resolve to no block share it (Codex review). */
|
|
3519
|
+
export function sameSupply(workspaceDir, tenant, a, b) {
|
|
3520
|
+
const supplyOf = (agent) => {
|
|
3521
|
+
const agentDir = path.join(workspaceDir, "agents", agent);
|
|
3522
|
+
let own;
|
|
3523
|
+
try {
|
|
3524
|
+
own = matter(fs.readFileSync(path.join(agentDir, "agent.md"), "utf8")).data.provider;
|
|
3525
|
+
}
|
|
3526
|
+
catch {
|
|
3527
|
+
own = undefined;
|
|
3528
|
+
}
|
|
3529
|
+
return stableJson(own ?? workspaceFrontmatter(agentDir, tenant).provider ?? null);
|
|
3530
|
+
};
|
|
3531
|
+
return supplyOf(a) === supplyOf(b);
|
|
3532
|
+
}
|
|
3533
|
+
function stableJson(v) {
|
|
3534
|
+
if (Array.isArray(v))
|
|
3535
|
+
return `[${v.map(stableJson).join(",")}]`;
|
|
3536
|
+
if (v && typeof v === "object") {
|
|
3537
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stableJson(v[k])}`).join(",")}}`;
|
|
3538
|
+
}
|
|
3539
|
+
return JSON.stringify(v) ?? "null";
|
|
3540
|
+
}
|
|
3513
3541
|
/** A refusal of the credential itself, in any of the wordings Claude Code
|
|
3514
3542
|
* has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
|
|
3515
3543
|
* step started one second before the 09:07:52Z refresh) matched none of the
|
|
@@ -5017,6 +5045,19 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
5017
5045
|
save();
|
|
5018
5046
|
if (run.stopRequested)
|
|
5019
5047
|
break;
|
|
5048
|
+
// A spent usage window resets in hours or days, never within
|
|
5049
|
+
// the retry waits below: another attempt is refused the same
|
|
5050
|
+
// way (abandon-desk's collector ran twice on 23 Oct 2026).
|
|
5051
|
+
const lastError = [...step.events].reverse().find((e) => e.type === "error")?.text;
|
|
5052
|
+
if (platformCause(lastError)?.key === "model-cap") {
|
|
5053
|
+
step.events.push({
|
|
5054
|
+
t: new Date().toISOString(),
|
|
5055
|
+
type: "info",
|
|
5056
|
+
text: `not retried — the model usage cap is reached and will not reset within a retry's wait (${attempts - attempt} left unused)`,
|
|
5057
|
+
});
|
|
5058
|
+
save();
|
|
5059
|
+
break;
|
|
5060
|
+
}
|
|
5020
5061
|
// The retry policy. An attempt the cluster ended for want of
|
|
5021
5062
|
// memory or disk is retried one size class up — the same
|
|
5022
5063
|
// size would end the same way. Every retry waits: 15 s, 30 s,
|
|
@@ -5087,6 +5128,21 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
5087
5128
|
});
|
|
5088
5129
|
return;
|
|
5089
5130
|
}
|
|
5131
|
+
// A rescuer on the same model supply hits the same wall. On 23 Oct
|
|
5132
|
+
// 2026 every desk's fixer started after a spent weekly window, was
|
|
5133
|
+
// refused the same way, and the issue read "fixer also failed"
|
|
5134
|
+
// instead of the cap. A rescuer on a different supply still tries.
|
|
5135
|
+
const lastError = [...step.events].reverse().find((e) => e.type === "error")?.text;
|
|
5136
|
+
const wall = platformCause(lastError);
|
|
5137
|
+
if (wall && SAME_SUPPLY_WALLS.has(wall.key) && sameSupply(pDir, tenant, step.agent, step.onFail)) {
|
|
5138
|
+
step.events.push({
|
|
5139
|
+
t: new Date().toISOString(),
|
|
5140
|
+
type: "info",
|
|
5141
|
+
text: `not handing to [[${step.onFail}]] (on-fail): ${wall.label.toLowerCase()}, and it runs on the same model supply`,
|
|
5142
|
+
});
|
|
5143
|
+
save();
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5090
5146
|
// One rescue per failure, across drives. A worker that died
|
|
5091
5147
|
// mid-rescue left an unfinished record behind it; the drive that
|
|
5092
5148
|
// resumes picks that one up instead of splicing another — a crash
|
package/package.json
CHANGED
package/src/issues.ts
CHANGED
|
@@ -134,7 +134,10 @@ const PLATFORM_CAUSES: (PlatformCause & { test: RegExp })[] = [
|
|
|
134
134
|
key: "model-cap",
|
|
135
135
|
label: "The model usage cap is reached",
|
|
136
136
|
fix: "runs resume when the cap resets; nothing to change in the desks.",
|
|
137
|
-
|
|
137
|
+
// Also the runner's own sentence for a spent subscription window
|
|
138
|
+
// (refusal.ts explainRefusal): on 23 Oct 2026 "the 7-day window is 100%
|
|
139
|
+
// used" filed five desks as their own failures.
|
|
140
|
+
test: /weekly (usage )?limit|usage limit (reached|exceeded)|reached your (weekly|usage) limit|window is (100% used|spent)/i,
|
|
138
141
|
},
|
|
139
142
|
{
|
|
140
143
|
// The provider out of capacity is not the account's cap: it passes in
|
package/src/run-container.ts
CHANGED
|
@@ -904,8 +904,11 @@ export const RETRY_SH = `retry() { what=$1; shift; n=1; until "$@"; do if [ "$n"
|
|
|
904
904
|
* and make one browse call per engine on the full image:
|
|
905
905
|
* docker buildx imagetools inspect node:22-trixie-slim --format '{{json .Manifest}}' | jq -r .digest
|
|
906
906
|
* The digest names the multi-arch index, so the one pin serves amd64 and
|
|
907
|
-
* arm64 alike.
|
|
908
|
-
|
|
907
|
+
* arm64 alike.
|
|
908
|
+
*
|
|
909
|
+
* 23 Oct 2026: Node 22.23.4 (the 21 Oct security release: Brotli decoder
|
|
910
|
+
* RCE, TLS hostname bypasses); the pin before carried 22.23.3. */
|
|
911
|
+
export const NODE_BASE = "node:22-trixie-slim@sha256:8eb619e402eaede1748a3c8173bffab391592812e85136452f781741c7242ddf";
|
|
909
912
|
|
|
910
913
|
// Two images from one file, as build targets.
|
|
911
914
|
//
|
package/src/runner.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { latestRevisionId } from "./history.ts";
|
|
7
7
|
import { recordCallIo, redactionsFor } from "./capture.ts";
|
|
8
|
-
import { AUTH_REFUSAL } from "./issues.ts";
|
|
8
|
+
import { AUTH_REFUSAL, platformCause } from "./issues.ts";
|
|
9
9
|
import { hookNames } from "./hooks.ts";
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
11
|
import fs from "node:fs";
|
|
@@ -3717,6 +3717,35 @@ function platformFallbackEnv(): Record<string, string> | null {
|
|
|
3717
3717
|
const CREDENTIAL_ROTATION_WAIT_MS = 90_000;
|
|
3718
3718
|
const CREDENTIAL_POLL_MS = 2_000;
|
|
3719
3719
|
|
|
3720
|
+
/** Platform causes a rescuer on the same model supply cannot get past. */
|
|
3721
|
+
const SAME_SUPPLY_WALLS = new Set(["model-cap", "model-auth"]);
|
|
3722
|
+
|
|
3723
|
+
/** Whether two agents of one workspace call the same model supply: each
|
|
3724
|
+
* resolved the way agentContext does — its own provider: block, else the
|
|
3725
|
+
* workspace's — and compared in a stable key order. The account key is
|
|
3726
|
+
* shared by both, so two that resolve to no block share it (Codex review). */
|
|
3727
|
+
export function sameSupply(workspaceDir: string, tenant: string, a: string, b: string): boolean {
|
|
3728
|
+
const supplyOf = (agent: string): string => {
|
|
3729
|
+
const agentDir = path.join(workspaceDir, "agents", agent);
|
|
3730
|
+
let own: unknown;
|
|
3731
|
+
try {
|
|
3732
|
+
own = matter(fs.readFileSync(path.join(agentDir, "agent.md"), "utf8")).data.provider;
|
|
3733
|
+
} catch {
|
|
3734
|
+
own = undefined;
|
|
3735
|
+
}
|
|
3736
|
+
return stableJson(own ?? workspaceFrontmatter(agentDir, tenant).provider ?? null);
|
|
3737
|
+
};
|
|
3738
|
+
return supplyOf(a) === supplyOf(b);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
function stableJson(v: unknown): string {
|
|
3742
|
+
if (Array.isArray(v)) return `[${v.map(stableJson).join(",")}]`;
|
|
3743
|
+
if (v && typeof v === "object") {
|
|
3744
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stableJson((v as Record<string, unknown>)[k])}`).join(",")}}`;
|
|
3745
|
+
}
|
|
3746
|
+
return JSON.stringify(v) ?? "null";
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3720
3749
|
/** A refusal of the credential itself, in any of the wordings Claude Code
|
|
3721
3750
|
* has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
|
|
3722
3751
|
* step started one second before the 09:07:52Z refresh) matched none of the
|
|
@@ -5294,6 +5323,19 @@ function driveRunInner(
|
|
|
5294
5323
|
// stop that lands while the outward check was being written.
|
|
5295
5324
|
save();
|
|
5296
5325
|
if (run.stopRequested) break;
|
|
5326
|
+
// A spent usage window resets in hours or days, never within
|
|
5327
|
+
// the retry waits below: another attempt is refused the same
|
|
5328
|
+
// way (abandon-desk's collector ran twice on 23 Oct 2026).
|
|
5329
|
+
const lastError = [...step.events].reverse().find((e) => e.type === "error")?.text;
|
|
5330
|
+
if (platformCause(lastError)?.key === "model-cap") {
|
|
5331
|
+
step.events.push({
|
|
5332
|
+
t: new Date().toISOString(),
|
|
5333
|
+
type: "info",
|
|
5334
|
+
text: `not retried — the model usage cap is reached and will not reset within a retry's wait (${attempts - attempt} left unused)`,
|
|
5335
|
+
});
|
|
5336
|
+
save();
|
|
5337
|
+
break;
|
|
5338
|
+
}
|
|
5297
5339
|
// The retry policy. An attempt the cluster ended for want of
|
|
5298
5340
|
// memory or disk is retried one size class up — the same
|
|
5299
5341
|
// size would end the same way. Every retry waits: 15 s, 30 s,
|
|
@@ -5363,6 +5405,21 @@ function driveRunInner(
|
|
|
5363
5405
|
});
|
|
5364
5406
|
return;
|
|
5365
5407
|
}
|
|
5408
|
+
// A rescuer on the same model supply hits the same wall. On 23 Oct
|
|
5409
|
+
// 2026 every desk's fixer started after a spent weekly window, was
|
|
5410
|
+
// refused the same way, and the issue read "fixer also failed"
|
|
5411
|
+
// instead of the cap. A rescuer on a different supply still tries.
|
|
5412
|
+
const lastError = [...step.events].reverse().find((e) => e.type === "error")?.text;
|
|
5413
|
+
const wall = platformCause(lastError);
|
|
5414
|
+
if (wall && SAME_SUPPLY_WALLS.has(wall.key) && sameSupply(pDir, tenant, step.agent, step.onFail)) {
|
|
5415
|
+
step.events.push({
|
|
5416
|
+
t: new Date().toISOString(),
|
|
5417
|
+
type: "info",
|
|
5418
|
+
text: `not handing to [[${step.onFail}]] (on-fail): ${wall.label.toLowerCase()}, and it runs on the same model supply`,
|
|
5419
|
+
});
|
|
5420
|
+
save();
|
|
5421
|
+
return;
|
|
5422
|
+
}
|
|
5366
5423
|
// One rescue per failure, across drives. A worker that died
|
|
5367
5424
|
// mid-rescue left an unfinished record behind it; the drive that
|
|
5368
5425
|
// resumes picks that one up instead of splicing another — a crash
|