From 42848b68f9e10ebe2a9fba90c6e7bc8861d54860 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:19:17 +0200 Subject: [PATCH 1/5] Report the exit code the engine actually saw Four places were turning real information into "exit code 1, category shell", which is why that pair sat at the top of the dashboard while telling nobody anything. post_update_to_api defaulted its code argument to 1 and mapped anything non-numeric to 1. Both mean "no code was passed", which is not the same fact as "the command returned 1" -- and 1 is what bash returns for every ordinary failure, so the two were indistinguishable once written. They now report 199, which says what happened. The host read the container's code with `cat flag || echo "1"`, throwing away a code it already had: lxc-attach's own return value, taken a few lines earlier. It falls back to that now. Codes 113, 114, 122, 253 and 254 are the engine asking a question and being told no. They were filed as failures, so "exit 113" -- a user declining an under-provisioned update -- was one of the largest single signatures in the data, sitting under exit code 1. They join the signal codes in reporting "aborted", and categorize_error agrees with that branch so a run is not filed as one thing and described as another. 199 is added to both copies of the table: api/exitcodes.func and the fallback in core/error_handler.func that containers use when api.func is not loaded. The header pointed at docs/exit_codes.md for keeping them in step, which does not exist; it now names what actually has to move. --- api/exitcodes.func | 22 +++++++++++++++++++--- api/telemetry.func | 20 ++++++++++++++++---- core/error_handler.func | 1 + pve/backend.func | 7 ++++++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/api/exitcodes.func b/api/exitcodes.func index f18c15b..594cf4a 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -10,8 +10,14 @@ # function name or behaviour changed with it. # # Turns a numeric exit status into a sentence, and sorts it into a category -# for the telemetry field. Almost entirely a lookup table -- keep it in step -# with docs/exit_codes.md, which has drifted from it before. +# for the telemetry field. Almost entirely a lookup table, and the authoritative +# copy of it. +# +# Two things have to move with it. core/error_handler.func carries a fallback +# table for the container case, where this file is not loaded; and the aborted +# branch of post_update_to_api has to agree with the user_aborted codes below, +# or a run is filed as one thing and described as another. The pointer here used +# to name docs/exit_codes.md, which does not exist. # ============================================================================== [[ -n "${_API_EXITCODES_LOADED:-}" ]] && return 0 @@ -165,6 +171,14 @@ explain_exit_code() { 192) echo "MongoDB: Database not found" ;; 193) echo "MongoDB: Fatal query error" ;; + # --- Engine (199) --- + # Not a failure of the install. It means the engine reported a terminal status + # without a code to go with it -- the flag file was unreadable, or a caller + # passed nothing. It used to be reported as 1, which is also what bash returns + # for any ordinary command failure, so a lost code and a real general error + # were the same number. + 199) echo "Engine: exit code not reported" ;; + # --- Proxmox Custom Codes (200-231) --- 200) echo "Proxmox: Failed to create lock file" ;; 203) echo "Proxmox: Missing CTID variable" ;; @@ -263,7 +277,9 @@ categorize_error() { 150 | 151 | 153 | 154) echo "service" ;; 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; - 129 | 130 | 143) echo "user_aborted" ;; + # Kept in step with the aborted branch of post_update_to_api: a code that + # makes the run "aborted" must not describe itself as something else here. + 113 | 114 | 122 | 129 | 130 | 143 | 253 | 254) echo "user_aborted" ;; 134 | 137) echo "resource" ;; 139 | 141) echo "signal" ;; 1 | 2) echo "shell" ;; diff --git a/api/telemetry.func b/api/telemetry.func index d6a6b9e..a673a36 100644 --- a/api/telemetry.func +++ b/api/telemetry.func @@ -313,7 +313,11 @@ post_update_to_api() { command -v curl &>/dev/null || return 0 local status="${1:-failed}" - local raw_exit_code="${2:-1}" + # Deliberately not defaulted to 1. A caller with nothing to report and a + # caller reporting a genuine general error are different facts, and 1 is what + # bash returns for every ordinary command failure -- defaulting to it made + # the two indistinguishable in the data. + local raw_exit_code="${2-}" local force="${3:-}" POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} @@ -336,7 +340,10 @@ post_update_to_api() { elif [[ "$raw_exit_code" == "none" ]]; then exit_code=0 else - exit_code=1 + # Neither a number nor "none": the code was never passed, or was lost on + # the way up. 199 says exactly that, where 1 used to claim a general error + # the engine never observed. + exit_code=199 fi local pb_status error_raw="" @@ -349,9 +356,14 @@ post_update_to_api() { pb_status="aborted" ;; failed) - # Signal-based exits are user aborts, not installation failures + # Codes the engine raises when the person at the keyboard said no. A signal + # is the obvious case; 113, 114, 122 and 254 are the engine asking a + # question and being told no, and 253 is an update the user declined to + # migrate. Filing those as failures put user decisions in the failure rate: + # "exit 113" was one of the largest single signatures on the dashboard, + # sitting under exit code 1 in category "shell". case "$exit_code" in - 129 | 130 | 143) pb_status="aborted" ;; + 113 | 114 | 122 | 129 | 130 | 143 | 253 | 254) pb_status="aborted" ;; *) pb_status="failed" ;; esac ;; diff --git a/core/error_handler.func b/core/error_handler.func index 547b40e..23e4117 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -155,6 +155,7 @@ if ! declare -f explain_exit_code &>/dev/null; then 191) echo "MongoDB: Authentication failed (bad user/password)" ;; 192) echo "MongoDB: Database not found" ;; 193) echo "MongoDB: Fatal query error" ;; + 199) echo "Engine: exit code not reported" ;; 200) echo "Proxmox: Failed to create lock file" ;; 203) echo "Proxmox: Missing CTID variable" ;; 204) echo "Proxmox: Missing PCT_OSTYPE variable" ;; diff --git a/pve/backend.func b/pve/backend.func index dde65c2..14cd205 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1208,7 +1208,12 @@ PROFILE if [[ -n "${SESSION_ID:-}" ]]; then local error_flag="/root/.install-${SESSION_ID}.failed" if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then - install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") + # The flag holds the container's own code. When it cannot be read, or + # comes back as something that is not a number, fall back to what + # lxc-attach returned rather than to 1: that is a code we actually + # observed, and it is already in hand a few lines up. + install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null | tr -d '[:space:]') || true + [[ "$install_exit_code" =~ ^[0-9]+$ ]] || install_exit_code="${lxc_exit:-199}" pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true fi fi From b3849fd3f87b514d85a59549bd3ab14ced0c1a08 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:19:27 +0200 Subject: [PATCH 2/5] Stop attributing the engine's own repo to forks Runs bootstrapped without COMMUNITY_SCRIPTS_URL fall through to scraping /proc/$$/cmdline, and the bootstrap line names only the engine. So the scrape finds "community-scripts/core", which matched no known repo and landed in the fork branch: REPO_SOURCE=external, REPO_SLUG= community-scripts/core. That is not a fork. It gave the slug its own row on the dashboard -- 4,451 runs, second only to ProxmoxVE itself -- and counted every one of them as somebody else's code. core now joins the empty case and takes the hardcoded fallback, which CI rewrites from VED to VE on promotion. Method 0 already existed to prevent this and is untouched; these are the runs where it does not apply. --- api/sysinfo.func | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/sysinfo.func b/api/sysinfo.func index efd35e6..ea30d09 100644 --- a/api/sysinfo.func +++ b/api/sysinfo.func @@ -92,8 +92,16 @@ detect_repo_source() { REPO_SOURCE="ProxmoxVED" REPO_SLUG="community-scripts/ProxmoxVED" ;; - "") - # No URL detected — hardcoded fallback (CI rewrites VED → VE on promotion) + "" | community-scripts/core) + # No script repo identified — hardcoded fallback (CI rewrites VED → VE on + # promotion). + # + # core lands here rather than in the fork branch below. The bootstrap line + # names the engine, not the repo the ct/ script came from, so the scraping + # above finds "community-scripts/core" for every run where + # COMMUNITY_SCRIPTS_URL was not set. That is not a fork, and filing it as + # one attributed thousands of official runs to "external" and gave the + # slug its own row on the dashboard, second only to ProxmoxVE. REPO_SOURCE="ProxmoxVED" REPO_SLUG="community-scripts/ProxmoxVED" ;; From 36219d6c315a7781c6ae6c16c8434e3b4b3d7eaf Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:30:50 +0200 Subject: [PATCH 3/5] Report the exit code the engine actually saw post_update_to_api defaulted its code argument to 1 and mapped anything non-numeric to 1, so "no code was passed" and "the command returned 1" became the same number -- and 1 is what bash returns for any ordinary failure. Both now report 199. The host read the container's code with `cat flag || echo "1"`, throwing away lxc-attach's own return value taken a few lines earlier. It falls back to that instead. 113, 114, 122, 253 and 254 are the engine asking a question and being told no. Filing them as failures is why "exit 113" was one of the largest signatures in the data, sitting under exit code 1. They now report aborted, and categorize_error agrees. 199 is added to both copies of the table, including the container fallback in core/error_handler.func. --- api/exitcodes.func | 8 ++++++-- api/telemetry.func | 9 +++++---- core/error_handler.func | 1 + pve/backend.func | 4 +++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/api/exitcodes.func b/api/exitcodes.func index f18c15b..045e61f 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -11,7 +11,8 @@ # # Turns a numeric exit status into a sentence, and sorts it into a category # for the telemetry field. Almost entirely a lookup table -- keep it in step -# with docs/exit_codes.md, which has drifted from it before. +# with the fallback copy in core/error_handler.func and with the aborted branch +# of post_update_to_api. # ============================================================================== [[ -n "${_API_EXITCODES_LOADED:-}" ]] && return 0 @@ -165,6 +166,9 @@ explain_exit_code() { 192) echo "MongoDB: Database not found" ;; 193) echo "MongoDB: Fatal query error" ;; + # --- Engine (199) --- + 199) echo "Engine: exit code not reported" ;; + # --- Proxmox Custom Codes (200-231) --- 200) echo "Proxmox: Failed to create lock file" ;; 203) echo "Proxmox: Missing CTID variable" ;; @@ -263,7 +267,7 @@ categorize_error() { 150 | 151 | 153 | 154) echo "service" ;; 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; - 129 | 130 | 143) echo "user_aborted" ;; + 113 | 114 | 122 | 129 | 130 | 143 | 253 | 254) echo "user_aborted" ;; 134 | 137) echo "resource" ;; 139 | 141) echo "signal" ;; 1 | 2) echo "shell" ;; diff --git a/api/telemetry.func b/api/telemetry.func index d6a6b9e..6299d30 100644 --- a/api/telemetry.func +++ b/api/telemetry.func @@ -313,7 +313,8 @@ post_update_to_api() { command -v curl &>/dev/null || return 0 local status="${1:-failed}" - local raw_exit_code="${2:-1}" + # Not defaulted: "no code passed" and "the command returned 1" differ. + local raw_exit_code="${2-}" local force="${3:-}" POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} @@ -336,7 +337,7 @@ post_update_to_api() { elif [[ "$raw_exit_code" == "none" ]]; then exit_code=0 else - exit_code=1 + exit_code=199 fi local pb_status error_raw="" @@ -349,9 +350,9 @@ post_update_to_api() { pb_status="aborted" ;; failed) - # Signal-based exits are user aborts, not installation failures + # Signals and declined prompts are user aborts, not installation failures case "$exit_code" in - 129 | 130 | 143) pb_status="aborted" ;; + 113 | 114 | 122 | 129 | 130 | 143 | 253 | 254) pb_status="aborted" ;; *) pb_status="failed" ;; esac ;; diff --git a/core/error_handler.func b/core/error_handler.func index 547b40e..23e4117 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -155,6 +155,7 @@ if ! declare -f explain_exit_code &>/dev/null; then 191) echo "MongoDB: Authentication failed (bad user/password)" ;; 192) echo "MongoDB: Database not found" ;; 193) echo "MongoDB: Fatal query error" ;; + 199) echo "Engine: exit code not reported" ;; 200) echo "Proxmox: Failed to create lock file" ;; 203) echo "Proxmox: Missing CTID variable" ;; 204) echo "Proxmox: Missing PCT_OSTYPE variable" ;; diff --git a/pve/backend.func b/pve/backend.func index dde65c2..005ba19 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1208,7 +1208,9 @@ PROFILE if [[ -n "${SESSION_ID:-}" ]]; then local error_flag="/root/.install-${SESSION_ID}.failed" if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then - install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") + # Unreadable or not a number: fall back to lxc-attach's own code. + install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null | tr -d '[:space:]') || true + [[ "$install_exit_code" =~ ^[0-9]+$ ]] || install_exit_code="${lxc_exit:-199}" pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true fi fi From 5ce7aa6804fb8e1ab24a6c46d522920b858b0f9e Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:30:58 +0200 Subject: [PATCH 4/5] Stop attributing the engine's own repo to forks Runs bootstrapped without COMMUNITY_SCRIPTS_URL fall through to scraping /proc/$$/cmdline, where the bootstrap line names only the engine. The scrape finds "community-scripts/core", which matched no known repo and landed in the fork branch as REPO_SOURCE=external. That gave the slug its own row on the dashboard -- 4,451 runs, second only to ProxmoxVE -- and counted them all as somebody else's code. core now joins the empty case and takes the hardcoded fallback, which CI rewrites from VED to VE on promotion. --- api/sysinfo.func | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/sysinfo.func b/api/sysinfo.func index efd35e6..c31807d 100644 --- a/api/sysinfo.func +++ b/api/sysinfo.func @@ -92,8 +92,10 @@ detect_repo_source() { REPO_SOURCE="ProxmoxVED" REPO_SLUG="community-scripts/ProxmoxVED" ;; - "") - # No URL detected — hardcoded fallback (CI rewrites VED → VE on promotion) + "" | community-scripts/core) + # No script repo identified — hardcoded fallback (CI rewrites VED → VE on + # promotion). core is the engine's own repo, not a fork: the bootstrap line + # names it whenever COMMUNITY_SCRIPTS_URL is unset. REPO_SOURCE="ProxmoxVED" REPO_SLUG="community-scripts/ProxmoxVED" ;; From c2d7a5bb7bb46c9a9339e351120da1c90c2ed0bd Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:47:22 +0200 Subject: [PATCH 5/5] Add _setup_npm to the lib API snapshot It arrived in lib/runtime.func with #16 and the snapshot was not regenerated, so "Each loader still defines its whole API" fails on main and on every branch opened from it. --- lib/API.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/API.txt b/lib/API.txt index d1439e0..adf6d9a 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -30,6 +30,7 @@ _setup_intel_arc _setup_intel_legacy _setup_intel_modern _setup_mariadb_runtime_dir +_setup_npm _setup_nvidia_gpu _setup_rocm _tools_cleanup_temp_dirs