fix: improve npm install resilience on low-memory systems#39463
fix: improve npm install resilience on low-memory systems#39463NewdlDewdl wants to merge 6 commits into
Conversation
|
Good approach! Handling OOM issues during npm install is important for production deployments. 👍 |
Greptile SummaryThis PR improves resilience of Two logic bugs were identified in the new code:
Confidence Score: 2/5
Last reviewed commit: 83ef500 |
| npm_log_indicates_oom_killed() { | ||
| local log="$1" | ||
| # Check if the process was killed (exit code 137 = SIGKILL, often OOM) | ||
| # The install.sh script will show "Killed" in the error output | ||
| if [[ ! -f "$log" ]]; then | ||
| return 1 | ||
| fi | ||
| # Empty or very small log files often indicate OOM kill | ||
| local log_size | ||
| log_size="$(wc -c < "$log" 2>/dev/null || echo "0")" | ||
| [[ "$log_size" -lt 100 ]] |
There was a problem hiding this comment.
Unreliable OOM detection heuristic
The function assumes any failed log file under 100 bytes means the process was OOM-killed. This is a very weak signal that will produce false positives for many other npm failure modes (e.g., network errors, permission errors, or a very fast failure). An OOM-killed process that had already written substantial output to the log would also be missed.
A more reliable approach is to check for the literal "Killed" string in the log output (which the Linux kernel prints to stderr when a process is SIGKILL'd), or to capture and inspect the actual exit code of the npm command.
| npm_log_indicates_oom_killed() { | |
| local log="$1" | |
| # Check if the process was killed (exit code 137 = SIGKILL, often OOM) | |
| # The install.sh script will show "Killed" in the error output | |
| if [[ ! -f "$log" ]]; then | |
| return 1 | |
| fi | |
| # Empty or very small log files often indicate OOM kill | |
| local log_size | |
| log_size="$(wc -c < "$log" 2>/dev/null || echo "0")" | |
| [[ "$log_size" -lt 100 ]] | |
| npm_log_indicates_oom_killed() { | |
| local log="$1" | |
| if [[ ! -f "$log" ]]; then | |
| return 1 | |
| fi | |
| # "Killed" is printed by the shell/kernel when a process receives SIGKILL (exit 137) | |
| grep -qE '^Killed$|^Killed |npm ERR! signal SIGKILL' "$log" 2>/dev/null | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/install.sh
Line: 749-759
Comment:
**Unreliable OOM detection heuristic**
The function assumes any failed log file under 100 bytes means the process was OOM-killed. This is a very weak signal that will produce false positives for many other npm failure modes (e.g., network errors, permission errors, or a very fast failure). An OOM-killed process that had already written substantial output to the log would also be missed.
A more reliable approach is to check for the literal "Killed" string in the log output (which the Linux kernel prints to stderr when a process is SIGKILL'd), or to capture and inspect the actual exit code of the npm command.
```suggestion
npm_log_indicates_oom_killed() {
local log="$1"
if [[ ! -f "$log" ]]; then
return 1
fi
# "Killed" is printed by the shell/kernel when a process receives SIGKILL (exit 137)
grep -qE '^Killed$|^Killed |npm ERR! signal SIGKILL' "$log" 2>/dev/null
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in 8ab82a8: removed the tiny-log-size heuristic and switched OOM detection to explicit signals only (captured npm exit code 137 + SIGKILL/'Killed'/OOM markers in output).
| local swap_total_kb | ||
| swap_total_kb="$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")" | ||
| [[ "$swap_total_kb" -ge 1048576 ]] # At least 1GB swap |
There was a problem hiding this comment.
SwapTotal checked instead of SwapFree
SwapTotal is the total swap partition/file size, which is non-zero even when all swap space is currently exhausted. On a system where 1 GB of swap exists but is 100% in use, this function returns true (sufficient swap), and the OOM retry path falls into the else branch telling the user to upgrade their droplet — even though creating additional swap would actually help.
SwapFree (free/available swap) is the correct field to check here.
| local swap_total_kb | |
| swap_total_kb="$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")" | |
| [[ "$swap_total_kb" -ge 1048576 ]] # At least 1GB swap | |
| swap_total_kb="$(awk '/SwapFree/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")" |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/install.sh
Line: 696-698
Comment:
**`SwapTotal` checked instead of `SwapFree`**
`SwapTotal` is the total swap partition/file size, which is non-zero even when all swap space is currently exhausted. On a system where 1 GB of swap exists but is 100% in use, this function returns `true` (sufficient swap), and the OOM retry path falls into the `else` branch telling the user to upgrade their droplet — even though creating additional swap would actually help.
`SwapFree` (free/available swap) is the correct field to check here.
```suggestion
swap_total_kb="$(awk '/SwapFree/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")"
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch - fixed in 8ab82a8. has_sufficient_swap now checks SwapFree (available swap) instead of SwapTotal, so exhausted swap no longer gets treated as sufficient.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 83ef500744
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| GIT_DIR=${OPENCLAW_GIT_DIR:-$GIT_DIR_DEFAULT} | ||
| GIT_UPDATE=${OPENCLAW_GIT_UPDATE:-1} | ||
| SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}" | ||
| SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-0}" |
There was a problem hiding this comment.
Restore SHARP_IGNORE_GLOBAL_LIBVIPS default to 1
This default flip likely inverts the intended sharp behavior: SHARP_IGNORE_GLOBAL_LIBVIPS=1 is documented in this repo as the way to avoid native libvips builds and use prebuilt binaries (see docs/install/index.md and docs/install/installer.md), but setting the installer default to 0 makes installs prefer the global libvips path again when present. On machines with global libvips (a common macOS/Homebrew setup), this can reintroduce source builds and the same OOM/install instability this change is trying to fix.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 8ab82a8: restored installer default to SHARP_IGNORE_GLOBAL_LIBVIPS=1 and aligned the help text accordingly.
| # Empty or very small log files often indicate OOM kill | ||
| local log_size | ||
| log_size="$(wc -c < "$log" 2>/dev/null || echo "0")" | ||
| [[ "$log_size" -lt 100 ]] |
There was a problem hiding this comment.
Use explicit OOM signals instead of tiny-log heuristic
Classifying OOM solely by log_size < 100 creates false positives for non-OOM npm failures that emit short output (for example early shell/env/npm invocation errors). Because install_openclaw_npm treats this as definitive OOM and returns after memory advice, it skips normal recovery/diagnostic paths and can mislead users on healthy-memory systems; detection should key off explicit OOM indicators (like Killed/exit 137) rather than log length alone.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 8ab82a8. OOM detection now keys off explicit signals (exit 137 and SIGKILL/Killed/OOM indicators), not log length, so non-OOM short failures still follow normal diagnostics/recovery.
|
Addressed review feedback in 8ab82a8 for the OOM/swap logic:\n\n- Replaced tiny-log heuristic with explicit OOM signals (exit 137 plus SIGKILL/Killed/OOM markers).\n- Updated swap sufficiency check to use SwapFree instead of SwapTotal.\n- Restored installer default SHARP_IGNORE_GLOBAL_LIBVIPS=1 and updated usage text to match.\n\nNote: I could not push to the original PR head branch due permission denial (403), so this commit is available on my fork branch 0xsline:fix/issue-39447-npm-install-oom. |
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Swapfile created with potentially permissive permissions before chmod (permission-race info disclosure)
DescriptionThe installer creates and enables a system swap file ( On typical systems with Because swap files can later contain paged-out memory (potentially including secrets), this can lead to sensitive information disclosure even after Vulnerable code: maybe_sudo dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" oflag=excl status=none 2>/dev/null || return 1
maybe_sudo chmod 600 "$swap_file" || return 1
maybe_sudo mkswap "$swap_file" >/dev/null 2>&1 || return 1
maybe_sudo swapon "$swap_file" || return 1RecommendationEnsure the swapfile is created with restrictive permissions from the first moment it exists. Options:
create_swap_file() {
...
maybe_sudo bash -c 'umask 077 && dd if=/dev/zero of="$1" bs=1M count="$2" oflag=excl status=none' _ "$swap_file" "$swap_size_mb" || return 1
maybe_sudo chmod 600 "$swap_file" || return 1
...
}
maybe_sudo install -m 600 /dev/null "$swap_file" || return 1
maybe_sudo dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" conv=notrunc status=none || return 1Also consider using 2. 🟡 Installer creates and enables persistent unencrypted swap file without user opt-in
DescriptionThe installer adds automatic swap provisioning on Linux by creating /swapfile-openclaw (2GB) and enabling it via Security/privacy/system-integrity concerns:
Vulnerable code: local swap_file="/swapfile-openclaw"
local swap_size_mb=2048
...
maybe_sudo dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" oflag=excl ...
...
maybe_sudo mkswap "$swap_file"
maybe_sudo swapon "$swap_file"RecommendationRequire explicit user opt-in and ensure cleanup. Safer approach:
Example implementation: OPENCLAW_SWAP_FILE="/swapfile-openclaw"
OPENCLAW_SWAP_CREATED=0
cleanup_openclaw_swap() {
if [[ "$OPENCLAW_SWAP_CREATED" -eq 1 ]]; then
maybe_sudo swapoff "$OPENCLAW_SWAP_FILE" 2>/dev/null || true
maybe_sudo rm -f "$OPENCLAW_SWAP_FILE" 2>/dev/null || true
fi
}
trap cleanup_openclaw_swap EXIT
create_swap_file() {
[[ "${OPENCLAW_ALLOW_SWAP:-0}" == "1" ]] || return 1
...
maybe_sudo swapon "$OPENCLAW_SWAP_FILE" || return 1
OPENCLAW_SWAP_CREATED=1
}Analyzed PR: #39463 at commit Last updated on: 2026-04-22T07:28:10Z |
|
Addressed the OOM/swap review feedback in 193085366f923ef268d7d3f498cd0be9ac07846d and pushed to this PR branch.\n\nChanges:\n- OOM detection now checks explicit signals (exit 137, SIGKILL/Killed/OOM markers) instead of tiny-log heuristics.\n- Swap sufficiency on Linux now uses SwapFree (available swap) instead of SwapTotal.\n- Restored installer default SHARP_IGNORE_GLOBAL_LIBVIPS=1 and aligned usage text.\n\nValidation:\n- Ran full quality gate on the branch (install/build/format:check/tsgo/lint/test): PASS. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 193085366f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return 1 | ||
| fi | ||
|
|
||
| require_sudo |
There was a problem hiding this comment.
Avoid hard-exiting when swap setup lacks sudo
install_openclaw_npm treats low-memory preparation as best-effort (setup_low_memory_environment || true), but this path can still terminate the whole installer because create_swap_file invokes require_sudo, which calls exit 1 if sudo is missing. On low-memory Linux systems running as non-root without sudo (common in minimal/container images), the installer now aborts before attempting npm install, even when a user-local npm prefix would otherwise work. This should return a failure status instead of exiting so normal install/recovery logic can continue.
Useful? React with 👍 / 👎.
|
Addressed the latest review feedback in 99b84ad2b and pushed to this PR branch. What changed:
Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99b84ad2b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| maybe_sudo dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" oflag=excl status=none 2>/dev/null || return 1 | ||
| maybe_sudo chmod 600 "$swap_file" || return 1 | ||
| maybe_sudo mkswap "$swap_file" >/dev/null 2>&1 || return 1 | ||
| maybe_sudo swapon "$swap_file" || return 1 |
There was a problem hiding this comment.
Clean up partial swap file after swapon failure
If dd/mkswap succeed but swapon fails (a common case in containers or restricted VMs without CAP_SYS_ADMIN), this function returns immediately and leaves /swapfile-openclaw on disk. Subsequent installer runs then hit the existing-file guard and skip automatic swap setup forever, while still consuming ~2GB of disk, so the recovery path introduced here can get stuck until the user manually removes the file.
Useful? React with 👍 / 👎.
| avail_kb="$(awk '/MemAvailable/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")" | ||
| echo $((avail_kb / 1024)) |
There was a problem hiding this comment.
Detect low memory from cgroup limits, not only /proc/meminfo
get_available_memory_mb uses /proc/meminfo MemAvailable, which often reports host memory rather than the container’s cgroup limit. On memory-limited containers this can overreport available RAM, causing the installer to skip swap setup and then treat an OOM-killed install as a hard failure instead of attempting the swap retry path. Reading cgroup memory limits when present would avoid this false “enough memory” classification.
Useful? React with 👍 / 👎.
|
Contributor cycle check-in: CI is currently red on this PR, and automation cannot rerun the failed workflow because this account lacks repository admin rerun permissions (GitHub returned: 'Must have admin rights to Repository'). No code changes were pushed in this cycle. If maintainers want this PR kept active, please rerun CI from the repo side and I can follow up with fixes from the resulting failures. |
99b84ad to
2d609e0
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d609e048f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fi | ||
|
|
||
| # Look for explicit OOM/SIGKILL indicators from shell/npm output. | ||
| grep -Eiq '(^|[[:space:]])Killed($|[[:space:]])|npm (ERR!|error) signal SIGKILL|signal SIGKILL|out of memory|oom' "$log" 2>/dev/null |
There was a problem hiding this comment.
Restrict OOM log matching to explicit tokens
The new OOM detector matches any occurrence of oom anywhere in the npm log, so unrelated strings like room or package names such as sonic-boom satisfy this check. When that happens on normal-memory hosts (available_mb >= 2048), install_openclaw_npm takes the OOM path and returns early with memory advice, skipping the normal build-tool remediation and detailed npm diagnostics for the real failure cause. Please tighten this regex to word-bounded OOM indicators (or specific npm signal lines) so non-OOM failures are not misclassified.
Useful? React with 👍 / 👎.
|
Addressed the Codex review note about broad OOM matching. Change made
Verification run locally
I pushed the fix commit to this PR branch for CI re-evaluation. |
Addresses openclaw#39447 - npm install killed on Ubuntu 24.04 droplet Problem: - npm install process killed (OOM) on low-memory VPS instances - SHARP_IGNORE_GLOBAL_LIBVIPS=1 forced sharp build from source - Sharp builds are extremely memory-intensive (requires ~2GB+ RAM) - No memory detection or swap mitigation Solution: 1. Change SHARP_IGNORE_GLOBAL_LIBVIPS default from 1 to 0 - Prefers prebuilt sharp binaries (much faster, less memory) - Only builds from source when env var explicitly set to 1 2. Add memory detection and swap file creation: - Detects available memory on Linux and macOS - Auto-creates 2GB swap file on low-memory Linux systems - Runs setup_low_memory_environment() before npm install 3. Detect and handle OOM/Killed scenarios: - npm_log_indicates_oom_killed() detects empty/killed logs - Provides actionable error messages with memory info - Suggests manual swap creation steps if auto-setup fails 4. Enhanced retry logic: - Retries npm install after swap creation - Clear status messages for swap/build-tool fix attempts - Better error diagnostics for memory-related failures This change makes OpenClaw installable on DigitalOcean's smallest droplets and other low-memory VPS instances, while maintaining the option to build from source when needed. Ref: openclaw#39447
7fa09e0 to
077e6f6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 077e6f656a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| route = { | ||
| ...route, | ||
| agentId: topicAgentId, | ||
| sessionKey: buildAgentSessionKey({ | ||
| agentId: topicAgentId, |
There was a problem hiding this comment.
Keep sessionKey binding before deriving topic route policy
This change removes the local sessionKey binding in the topic override block, but the code below still uses sessionKey to compute lastRoutePolicy. When topicAgentId is set, that branch now hits ReferenceError: sessionKey is not defined, which breaks Telegram topic-bound routing instead of preserving the configured topic session.
Useful? React with 👍 / 👎.
| DEFAULT_ACCOUNT_ID, | ||
| resolveAgentIdFromSessionKey, | ||
| sanitizeAgentId, | ||
| } from "openclaw/plugin-sdk/routing"; | ||
| import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; | ||
| import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; | ||
| } from "../../../src/routing/session-key.js"; |
There was a problem hiding this comment.
Restore helper imports still referenced in conversation routing
The import rewrite drops the logVerbose and normalizeLowercaseStringOrEmpty imports, but both identifiers are still referenced later in this module (resolveTelegramConversationRoute and resolveTelegramConversationBaseSessionKey). That leaves unresolved bindings in runtime code paths and prevents this route module from executing correctly.
Useful? React with 👍 / 👎.
|
Contributor cycle follow-up:\n\nI reviewed the new Codex/Aisle feedback first. The two Codex P1 thread comments were against commit ; current PR head already has both fixes in :\n- is restored before \n- and imports are present\n\nI also ran the full quality gate before attempting any further push. Baseline is currently red on (,
[test-parallel] start unit-fast-batch-1 workers=2 filters=all �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cli/daemon-cli/response.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 4�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m41 passed�[39m�[22m�[90m (41)�[39m [test-parallel] done unit-fast-batch-3 code=0 elapsed=27.5s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/acp/translator.session-rate-limit.test.ts �[2m(�[22m�[2m21 tests�[22m�[2m)�[22m�[32m 16�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m42 passed�[39m�[22m�[90m (42)�[39m [test-parallel] done unit-fast-batch-4 code=0 elapsed=22.4s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cli/prompt.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 4�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m41 passed�[39m�[22m�[90m (41)�[39m �[32m✓�[39m src/channels/plugins/binding-targets.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 51104�[2mms�[22m�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/config/allowed-values.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m44 passed�[39m�[22m�[90m (44)�[39m [test-parallel] done unit-fast-batch-5 code=0 elapsed=22.1s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/channels/plugins/group-policy-warnings.test.ts �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[32m 8�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m45 passed�[39m�[22m�[90m (45)�[39m �[32m✓�[39m src/config/merge-patch.proto-pollution.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/config/sessions/explicit-session-key-normalization.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 28�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m40 passed�[39m�[22m�[90m (40)�[39m [test-parallel] done unit-fast-batch-7 code=0 elapsed=20.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/isolated-agent/subagent-followup.test.ts �[2m(�[22m�[2m24 tests�[22m�[2m)�[22m�[32m 22�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m41 passed�[39m�[22m�[90m (41)�[39m �[32m✓�[39m src/cron/isolated-agent.delivery-awareness.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 156�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m43 passed�[39m�[22m�[90m (43)�[39m [test-parallel] done unit-fast-batch-8 code=0 elapsed=124.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/service.prevents-duplicate-timers.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 41�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m50 passed�[39m�[22m�[90m (50)�[39m �[32m✓�[39m src/daemon/runtime-hints.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/hooks/fire-and-forget.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 2�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m39 passed�[39m�[22m�[90m (39)�[39m �[32m✓�[39m src/infra/canvas-host-url.test.ts �[2m(�[22m�[2m8 tests�[22m�[2m)�[22m�[32m 2�[2mms�[22m�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/channel-summary.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[32m 3�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m42 passed�[39m�[22m�[90m (42)�[39m [test-parallel] done unit-fast-batch-12 code=0 elapsed=21.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/image-generation/runtime.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 31884�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m40 passed�[39m�[22m�[90m (40)�[39m [test-parallel] done unit-fast-batch-11 code=0 elapsed=44.1s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/network-interfaces.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m39 passed�[39m�[22m�[90m (39)�[39m [test-parallel] done unit-fast-batch-13 code=0 elapsed=121.6s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/resolve-system-bin.test.ts �[2m(�[22m�[2m21 tests�[22m�[2m)�[22m�[32m 7�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m49 passed�[39m�[22m�[90m (49)�[39m [test-parallel] done unit-fast-batch-14 code=0 elapsed=120.4s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/provider-usage.shared.test.ts �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[32m 16�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m43 passed�[39m�[22m�[90m (43)�[39m [test-parallel] done unit-fast-batch-16 code=0 elapsed=23.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/outbound/target-errors.test.ts �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m43 passed�[39m�[22m�[90m (43)�[39m �[32m✓�[39m src/infra/unhandled-rejections.fatal-detection.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[32m 16�[2mms�[22m�[39m �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/logging/logger-timestamp.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m41 passed�[39m�[22m�[90m (41)�[39m [test-parallel] done unit-fast-batch-18 code=0 elapsed=23.8s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/system-run-approval-mismatch.contract.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 2�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m42 passed�[39m�[22m�[90m (42)�[39m [test-parallel] done unit-fast-batch-17 code=0 elapsed=57.1s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/node-host/runner.credentials.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m43 passed�[39m�[22m�[90m (43)�[39m [test-parallel] done unit-fast-batch-20 code=0 elapsed=59.5s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/node-host/invoke.sanitize-env.test.ts �[2m(�[22m�[2m10 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m44 passed�[39m�[22m�[90m (44)�[39m [test-parallel] done unit-fast-batch-19 code=0 elapsed=69.9s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugins/wired-hooks-subagent.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m42 passed�[39m�[22m�[90m (42)�[39m [test-parallel] done unit-fast-batch-21 code=0 elapsed=62.1s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/tasks/task-registry.test.ts �[2m(�[22m�[2m32 tests�[22m�[2m)�[22m�[33m 564�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m36 passed�[39m�[22m�[90m (36)�[39m [test-parallel] done unit-fast-batch-23 code=0 elapsed=36.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/shared/net/ipv4.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m39 passed�[39m�[22m�[90m (39)�[39m [test-parallel] done unit-fast-batch-25 code=0 elapsed=19.3s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugins/runtime/types.contract.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 3�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m45 passed�[39m�[22m�[90m (45)�[39m [test-parallel] done unit-fast-batch-22 code=0 elapsed=62.9s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m packages/memory-host-sdk/src/host/embeddings-gemini.test.ts �[2m(�[22m�[2m31 tests�[22m�[2m)�[22m�[33m 310�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m40 passed�[39m�[22m�[90m (40)�[39m [test-parallel] done unit-fast-batch-24 code=0 elapsed=46.8s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m packages/plugin-package-contract/src/index.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m40 passed�[39m�[22m�[90m (40)�[39m �[32m✓�[39m test/scripts/test-extension.test.ts �[2m(�[22m�[2m11 tests�[22m�[2m)�[22m�[33m 416�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m31 passed�[39m�[22m�[90m (31)�[39m [test-parallel] done unit-fast-batch-27 code=0 elapsed=30.8s �[2m Test Files �[22m �[1m�[32m23 passed�[39m�[22m�[90m (23)�[39m [test-parallel] done unit-fast-batch-28 code=0 elapsed=24.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cli/qr-dashboard.integration.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[33m 17673�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-qr-dashboard.integration-isolated code=0 elapsed=22.0s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts �[2m(�[22m�[2m52 tests�[22m�[2m)�[22m�[32m 63�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-config.legacy-config-detection.rejects-routing-allowfrom-isolated code=0 elapsed=4.1s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/config/plugin-auto-enable.test.ts �[2m(�[22m�[2m38 tests�[22m�[2m)�[22m�[33m 11240�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-plugin-auto-enable-isolated code=0 elapsed=15.3s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/matrix-legacy-crypto.test.ts �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[32m 136�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-matrix-legacy-crypto-isolated code=0 elapsed=4.8s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/state-migrations.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 39�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-state-migrations-isolated code=0 elapsed=4.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugins/commands.test.ts �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[32m 18�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-commands-isolated code=0 elapsed=4.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugin-sdk/package-contract-guardrails.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[33m 30786�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-package-contract-guardrails-isolated code=0 elapsed=34.9s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/secrets/runtime.coverage.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[33m 682�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-runtime.coverage-isolated code=0 elapsed=5.3s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/secrets/runtime.test.ts �[2m(�[22m�[2m67 tests�[22m�[2m)�[22m�[33m 391�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-runtime-isolated code=0 elapsed=4.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/security/audit.test.ts �[2m(�[22m�[2m73 tests�[22m�[2m)�[22m�[33m 984�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-audit-isolated code=0 elapsed=21.4s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m test/extension-plugin-sdk-boundary.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 1150�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m2 passed�[39m�[22m�[90m (2)�[39m [test-parallel] done unit-heavy-1 code=0 elapsed=27.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/hooks/install.test.ts �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[33m 743�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m3 passed�[39m�[22m�[90m (3)�[39m [test-parallel] done unit-heavy-2 code=0 elapsed=12.2s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugin-sdk/index.bundle.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[33m 10381�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m2 passed�[39m�[22m�[90m (2)�[39m [test-parallel] done unit-heavy-3 code=0 elapsed=15.3s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/outbound/channel-resolution.test.ts �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[32m 42�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-channel-resolution-memory-isolated code=0 elapsed=4.7s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/isolated-agent/run.skill-filter.test.ts �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[32m 13�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-run.skill-filter-memory-isolated code=0 elapsed=24.5s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/infra/outbound/deliver.test.ts �[2m(�[22m�[2m45 tests�[22m�[2m)�[22m�[33m 820�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-deliver-memory-isolated code=0 elapsed=85.9s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[33m 436�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-isolated-agent.uses-last-non-empty-agent-text-as-memory-isolated code=0 elapsed=22.5s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[33m 466�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true-memory-isolated code=0 elapsed=90.3s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/cron/isolated-agent/run.cron-model-override.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-run.cron-model-override-memory-isolated code=0 elapsed=25.4s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugins/manifest-registry.test.ts �[2m(�[22m�[2m30 tests�[22m�[2m)�[22m�[32m 151�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-manifest-registry-memory-isolated code=0 elapsed=4.5s �[1m�[46m RUN �[49m�[22m �[36mv4.1.2 �[39m�[90m/Users/newdldewdl/openclaw-contrib�[39m �[32m✓�[39m src/plugins/install.test.ts �[2m(�[22m�[2m40 tests�[22m�[2m)�[22m�[33m 1597�[2mms�[22m�[39m �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m [test-parallel] done unit-install-memory-isolated code=0 elapsed=6.3s |
|
Codex automated review: keeping this open. Keep open. Current Best possible solution: Continue tracking this item until the missing behavior is implemented or a maintainer decides the product direction. What I checked:
Remaining risk / open question:
Codex Review notes: reviewed against 017252e4f8e7. |
|
Closing this as obsolete after maintainer verification. The linked issue #39447 is fixed for the current
I do not want to land the proposed auto-swap behavior from this PR for a now-unreproduced installer failure. It mutates host swap state during a generic installer run, and the main stated sharp premise is incorrect: sharp's docs say package managers select prebuilt binaries where available, and Thanks for the focused work here. The right follow-up, if this resurfaces, is a fresh failing log from current |
|
Obsolete: current latest installer succeeds under 512 MB/no-swap in live verification; see maintainer comment. |
Fixes #39447
Problem
npm install fails with "Killed" on low-memory VPS instances (e.g., DigitalOcean droplets with <2GB RAM):
Solution
1. Prefer prebuilt sharp binaries (memory-efficient)
SHARP_IGNORE_GLOBAL_LIBVIPSdefault from1to0SHARP_IGNORE_GLOBAL_LIBVIPS=12. Memory detection and swap file creation
get_available_memory_mb(): detects available RAM on Linux/macOSsetup_low_memory_environment(): auto-creates 2GB swap on systems with <2GB RAMcreate_swap_file(): creates /swapfile-openclaw with proper permissions3. OOM detection and recovery
npm_log_indicates_oom_killed(): detects when npm was killed by OOMTesting
Impact
Makes OpenClaw installable on DigitalOcean's smallest droplets and other low-memory VPS instances without manual swap configuration.
Ref: #39447