diff --git a/scripts/opencode-builder.sh b/scripts/opencode-builder.sh index 46d542a53..9b5c1da3d 100755 --- a/scripts/opencode-builder.sh +++ b/scripts/opencode-builder.sh @@ -45,28 +45,32 @@ IDLE_THRESHOLD_MS=180000 # 3 minutes — session considered idle if last_active # Script paths SCRIPT_DIR="/app/.opencode/skills/auto-agents-system/scripts" -# Resolve a stable tsx binary path. Using `npx --yes tsx` is unsafe under -# concurrency: when many invocations start simultaneously with a cold (or -# even partially-warm) npm cache, they race on writes to ~/.npm/_npx/.../ -# and corrupt each other with `ENOTEMPTY: rename '.../esbuild' ...` errors, -# crashing all but one and leaving the cache in a broken state for some time -# afterwards. We install tsx once into ~/.local/node_modules and call that -# binary directly, bypassing npx entirely. +# Make tsx discoverable on PATH. Running `npx --yes tsx` concurrently with +# a cold or partially-warm npm cache causes a race in `~/.npm/_npx//`: +# multiple npm processes simultaneously try to rename `node_modules/esbuild` +# to the same atomic-backup name and fail with +# `ENOTEMPTY: rename '.../esbuild' -> '.../esbuild-7kCFO8Lm'`, crashing +# all but one of them and leaving the cache broken for follow-on calls. +# When tsx is on PATH, npx skips its install/cache logic entirely and just +# execs the existing binary — so the race disappears without changing any +# call sites. We install tsx once into ~/.local/node_modules and prepend +# its bin dir to PATH for the rest of this script's process tree. TSX_PREFIX="${HOME}/.local" -TSX_BIN="${TSX_PREFIX}/node_modules/.bin/tsx" -if ! [[ -x "$TSX_BIN" ]]; then +TSX_BIN_DIR="${TSX_PREFIX}/node_modules/.bin" +if ! [[ -x "${TSX_BIN_DIR}/tsx" ]]; then printf "[%s] Installing tsx into %s …\n" "$(date +%H:%M:%S)" "$TSX_PREFIX" >&2 mkdir -p "$TSX_PREFIX" npm install --prefix "$TSX_PREFIX" --silent tsx >/dev/null 2>&1 \ || { printf "ERROR: failed to install tsx into %s\n" "$TSX_PREFIX" >&2; exit 1; } fi +export PATH="${TSX_BIN_DIR}:${PATH}" -SESSION_LIST="$TSX_BIN ${SCRIPT_DIR}/session_list.ts" -SESSION_FIND_PREFIX="$TSX_BIN ${SCRIPT_DIR}/session_find_by_prefix.ts" -SESSION_START="$TSX_BIN ${SCRIPT_DIR}/session_start.ts" -SESSION_MESSAGES="$TSX_BIN ${SCRIPT_DIR}/session_messages.ts" -SESSION_DELETE="$TSX_BIN ${SCRIPT_DIR}/session_delete.ts" -SESSION_STOP="$TSX_BIN ${SCRIPT_DIR}/session_stop.ts" +SESSION_LIST="npx --yes tsx ${SCRIPT_DIR}/session_list.ts" +SESSION_FIND_PREFIX="npx --yes tsx ${SCRIPT_DIR}/session_find_by_prefix.ts" +SESSION_START="npx --yes tsx ${SCRIPT_DIR}/session_start.ts" +SESSION_MESSAGES="npx --yes tsx ${SCRIPT_DIR}/session_messages.ts" +SESSION_DELETE="npx --yes tsx ${SCRIPT_DIR}/session_delete.ts" +SESSION_STOP="npx --yes tsx ${SCRIPT_DIR}/session_stop.ts" # ── Internal state ─────────────────────────────────────────────────────── SERVER_PID="" @@ -425,7 +429,7 @@ fetch_list_script() { fi local result - result=$(timeout "$SCRIPT_TIMEOUT_SEC" "$TSX_BIN" "$script_path" \ + result=$(timeout "$SCRIPT_TIMEOUT_SEC" npx --yes tsx "$script_path" \ --url "$forgejo_url" \ --pat "$forgejo_pat" \ --owner "$forgejo_owner" \ @@ -1076,7 +1080,7 @@ main_loop() { fi cache_keys+=("$cache_key") log "[pool:${prefix}] launching $(basename "$script_path") (cache key: ${cache_key})..." - timeout "$SCRIPT_TIMEOUT_SEC" "$TSX_BIN" "$script_path" \ + timeout "$SCRIPT_TIMEOUT_SEC" npx --yes tsx "$script_path" \ --url "$FORGEJO_URL" \ --pat "$FORGEJO_PAT" \ --owner "$FORGEJO_OWNER" \