diff --git a/README.md b/README.md index 46d4266..10e8e19 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,19 @@ cd /Volumes/Data/openclaw-setups/openclaw-setup-max cat README.md ``` +Existing install upgrade (in-place): + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +This update workflow also reapplies schedule/budget guardrails and self-heals gateway LaunchAgent issues for external-drive state dirs. + Primary docs: - `openclaw-setup-max/README.md` - `openclaw-setup-max/PRD.md` +- `openclaw-setup-max/docs/operations/UPGRADING.md` ### Copilot setup diff --git a/openclaw-setup-max/PRD.md b/openclaw-setup-max/PRD.md index 55967ae..e18a063 100644 --- a/openclaw-setup-max/PRD.md +++ b/openclaw-setup-max/PRD.md @@ -36,7 +36,7 @@ Operators need an easy way to: - As an operator, I can run one command to switch to free local models immediately. - As an operator, I can switch back to paid model for deep tasks. -- As an operator, I can set free mode from 9pm-7am automatically. +- As an operator, I can set free mode from 10pm-7am automatically. - As an operator, I get warned if expensive model remains active too long. - As a maintainer, I can copy this folder to another Mac and follow docs to bootstrap. @@ -104,6 +104,21 @@ Implementation: Implementation: - `README.md` +### FR-7 Existing-Install Upgrade Workflow +- Must provide one command for in-place upgrades on existing machines. +- Must back up OpenClaw config/session files before migration/update. +- Must run config migration (`openclaw doctor --fix`) before attempting update. +- Must update CLI using built-in updater with timeout + npm fallback. +- Must emit timestamped update logs for operator traceability. +- Must stop gateway before update and bring gateway back after update. +- Must ensure gateway LaunchAgent is loaded/running after update. +- If `~/.openclaw` is symlinked to `/Volumes/...`, must ensure gateway LaunchAgent logs use `/tmp/openclaw-gateway.launchd*.log` to avoid launchd `EX_CONFIG`. +- Must reinstall and kickstart budget/schedule guard LaunchAgents after update. + +Implementation: +- `scripts/update_openclaw.sh` +- `setup/setup_openclaw_ollama.sh` + ## 7) Non-Functional Requirements - NFR-1 Reliability: scripts should be safe to run repeatedly. @@ -154,11 +169,14 @@ Outputs: ## 10) Acceptance Criteria - AC-1: Running `bash ./scripts/model_profile_switch.sh free` changes default to `openrouter/qwen/qwen3-coder:free`. -- AC-2: Running `bash ./scripts/model_profile_switch.sh paid` restores default to `openrouter/qwen/qwen3-coder:free`. -- AC-3: With schedule enabled (`21`/`7`), profile changes correctly by local time window. +- AC-2: Running `bash ./scripts/model_profile_switch.sh paid` restores default to `openrouter/moonshotai/kimi-k2.5`. +- AC-3: With schedule enabled (`22`/`7`), profile changes correctly by local time window. - AC-4: If operator manually drifts model during scheduled window, schedule guard re-aligns on next run. - AC-5: Budget guard warns and reverts when high model remains active beyond thresholds. - AC-6: New operator can follow README from zero knowledge and complete setup without editing scripts. +- AC-7: Running `bash ./scripts/update_openclaw.sh` upgrades an existing install and prints a healthy gateway/model status at completion. +- AC-8: After update, schedule + budget LaunchAgents are loaded and can run immediately when kickstarted. +- AC-9: Upgrade output lines include timestamps to support restart/audit troubleshooting. ## 11) Risks and Mitigations @@ -170,6 +188,8 @@ Outputs: - Mitigation: prerequisite checks in README. - Risk: user confusion between ChatGPT subscription and API billing. - Mitigation: clear FAQ note in README. +- Risk: gateway LaunchAgent exits with `EX_CONFIG` when logs point into external-volume symlink paths. + - Mitigation: force gateway LaunchAgent stdout/stderr to `/tmp/openclaw-gateway.launchd*.log` when `~/.openclaw` resolves to `/Volumes/...`. ## 12) Rollout Plan diff --git a/openclaw-setup-max/README.md b/openclaw-setup-max/README.md index 80daed0..e13be5f 100644 --- a/openclaw-setup-max/README.md +++ b/openclaw-setup-max/README.md @@ -1,13 +1,13 @@ # OpenClaw Setup Max (Paid + Free Model Switching) This workspace runs OpenClaw with: -- A paid high-quality model profile (`openrouter/qwen/qwen3-coder:free`) +- A paid high-quality model profile (`openrouter/moonshotai/kimi-k2.5`) - A free OpenRouter profile - One-command live switching -- Optional automatic day/night switching (example: free from 9pm-7am) +- Optional automatic day/night switching (example: free from 10pm-7am) - Optional budget guard that warns and auto-reverts from expensive models -This guide is written for first-time users. +This guide covers both first-time setup and in-place upgrades. ## 0) Workspace Layout @@ -27,7 +27,7 @@ Everything else is grouped by function: ## 1) What You Get - `paid` profile: - - Primary: `openrouter/qwen/qwen3-coder:free` + - Primary: `openrouter/moonshotai/kimi-k2.5` - Fallbacks: `openrouter/qwen/qwen3-coder-next`, `openrouter/qwen/qwen3-14b` - `free` profile: - Primary: `openrouter/qwen/qwen3-coder:free` @@ -49,6 +49,9 @@ Profiles are defined in `config/model-profiles.config.json`. - `scripts/install_model_schedule_guard_launchd.sh`: installs schedule LaunchAgent - `scripts/install_model_budget_guard_launchd.sh`: installs budget LaunchAgent - `scripts/install_local_model_guardrails.sh`: installs both LaunchAgents +- `scripts/update_openclaw.sh`: in-place OpenClaw upgrade + migration workflow +- `docs/operations/UPGRADING.md`: upgrade runbook for existing installs +- `docs/operations/troubleshooting.md`: known issue recovery commands ## 3) Prerequisites @@ -70,6 +73,7 @@ Script behavior: - `openclaw` - `jq` - `python3` +- If `~/.openclaw` is symlinked to an external volume (`/Volumes/...`), setup patches gateway LaunchAgent logs to `/tmp/openclaw-gateway.launchd*.log` to avoid launchd `EX_CONFIG` failures. - It will prompt before: - Installing Ollama (if missing) - Pulling local Ollama models (large downloads) @@ -84,6 +88,45 @@ AUTO_YES=true bash ./setup/setup_openclaw_ollama.sh PULL_LOCAL_MODELS=false bash ./setup/setup_openclaw_ollama.sh ``` +## 3A) Update Existing Install (In-place) + +Use this for upgrades on an already-working machine: + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +What it handles: +- Backup of `~/.openclaw/openclaw.json` + main `sessions.json` +- Config migration (`openclaw doctor --fix --non-interactive`) +- CLI update with timeout + npm fallback +- Timestamped step logging in terminal output +- Explicit gateway stop/start +- Gateway LaunchAgent self-heal (install if missing) +- External-drive launchd log-path patch (`/tmp/openclaw-gateway.launchd*.log`) +- Reinstall + immediate kickstart of budget/schedule guards +- Final model/profile/gateway verification output + +Useful options: + +```bash +# Switch update channel +bash ./scripts/update_openclaw.sh --channel beta + +# Pin one update run to a version/tag +bash ./scripts/update_openclaw.sh --tag 2026.2.24 + +# Increase built-in updater timeout (seconds) +UPDATE_MAX_SECONDS=900 bash ./scripts/update_openclaw.sh + +# Save a timestamped run log file +bash ./scripts/update_openclaw.sh 2>&1 | tee "/tmp/openclaw-update-$(date +%Y%m%d-%H%M%S).log" +``` + +Detailed runbook: +- `docs/operations/UPGRADING.md` + ## 4) Quick Start (Most Common) Use env-driven sync + hygiene workflow (recommended after any model changes): @@ -145,7 +188,7 @@ If you want config-only change (no live session message), use: bash ./scripts/model_profile_switch.sh free --no-live ``` -## 6) Enable Schedule (Example: Free 9pm-7am) +## 6) Enable Schedule (Example: Free 10pm-7am) 1. Edit config: @@ -162,7 +205,7 @@ open config/model-schedule.config.json "dayProfile": "paid", "nightProfile": "free", "dayStartHour": 7, - "nightStartHour": 21 + "nightStartHour": 22 } ``` @@ -224,7 +267,7 @@ bash ./scripts/install_local_model_guardrails.sh Budget behavior from `config/model-budget-guard.config.json`: - Warn after 2 minutes on high-cost model -- Auto-revert to `ollama/qwen3:14b` after 45 minutes +- Auto-revert to `openrouter/qwen/qwen3-coder:free` after 45 minutes - Prevent repeated spam with minimum warning interval ## 9) View Current LaunchAgents @@ -239,6 +282,7 @@ Logs: ```bash tail -f /tmp/openclaw-model-schedule-guard.log /tmp/openclaw-model-schedule-guard.err.log tail -f /tmp/openclaw-model-budget-guard.log /tmp/openclaw-model-budget-guard.err.log +tail -f /tmp/openclaw-gateway.launchd.log /tmp/openclaw-gateway.launchd.err.log ``` ## 10) Manual Switching Tips diff --git a/openclaw-setup-max/docs/openclaw-setup-guide.md b/openclaw-setup-max/docs/openclaw-setup-guide.md index 9111e65..cb43fb7 100644 --- a/openclaw-setup-max/docs/openclaw-setup-guide.md +++ b/openclaw-setup-max/docs/openclaw-setup-guide.md @@ -49,6 +49,13 @@ openclaw gateway stop openclaw gateway restart ``` +**Repo-specific in-place upgrade (existing install):** +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` +This script stops gateway, upgrades OpenClaw, reapplies schedule/budget guardrails, and restarts gateway. + ### Directory Structure **Workspace Location:** diff --git a/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md b/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md index 7ee1194..0892ecf 100644 --- a/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md +++ b/openclaw-setup-max/docs/operations/MODEL_SWITCHING.md @@ -2,7 +2,7 @@ This setup adds two model profiles: -- `paid` -> `openrouter/qwen/qwen3-coder:free` with OpenRouter fallbacks +- `paid` -> `openrouter/moonshotai/kimi-k2.5` with OpenRouter fallbacks - `free` -> `openrouter/qwen/qwen3-coder:free` with OpenRouter fallback ## Recommended workflow (prevents legacy drift) @@ -40,7 +40,7 @@ Notes: bash ./scripts/model_profile_switch.sh status ``` -## Schedule auto-switching (example: free from 9pm-7am) +## Schedule auto-switching (example: free from 10pm-7am) 1. Edit config: @@ -57,7 +57,7 @@ Ensure: "dayProfile": "paid", "nightProfile": "free", "dayStartHour": 7, - "nightStartHour": 21 + "nightStartHour": 22 } ``` @@ -100,3 +100,14 @@ bash ./scripts/install_local_model_guardrails.sh launchctl bootout gui/$(id -u)/ai.openclaw.local.model-schedule-guard 2>/dev/null || true launchctl bootout gui/$(id -u)/ai.openclaw.local.model-budget-guard 2>/dev/null || true ``` + +## After OpenClaw upgrades + +Use the update workflow so guards/profile state are reapplied automatically: + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +The update script reinstalls both guard LaunchAgents and kickstarts them immediately, so budget/profile logic is re-evaluated in the same run. diff --git a/openclaw-setup-max/docs/operations/UPGRADING.md b/openclaw-setup-max/docs/operations/UPGRADING.md new file mode 100644 index 0000000..81d6ea1 --- /dev/null +++ b/openclaw-setup-max/docs/operations/UPGRADING.md @@ -0,0 +1,66 @@ +# OpenClaw Upgrade Runbook (Existing Install) + +Use this when OpenClaw is already installed and you want to upgrade in place. + +## Recommended command + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +## What `update_openclaw.sh` does + +- Backs up: + - `~/.openclaw/openclaw.json` + - `~/.openclaw/agents/main/sessions/sessions.json` +- Stops local model guard LaunchAgents and gateway before update +- Runs `openclaw doctor --fix --non-interactive` +- Runs `openclaw update` with timeout protection +- Falls back to `npm install -g openclaw@...` if updater hangs/fails +- Prints timestamped step logs for restart/update traceability +- Starts gateway and installs service if missing +- If `~/.openclaw` is symlinked to `/Volumes/...`, patches gateway LaunchAgent logs to: + - `/tmp/openclaw-gateway.launchd.log` + - `/tmp/openclaw-gateway.launchd.err.log` +- Reinstalls schedule + budget guard LaunchAgents +- Kickstarts both guards immediately so profile/budget logic is applied now +- Prints gateway + model/guard status summary + +## Useful options + +```bash +# switch update channel +bash ./scripts/update_openclaw.sh --channel beta + +# one-off version/tag +bash ./scripts/update_openclaw.sh --tag 2026.2.24 + +# increase built-in update timeout (seconds) +UPDATE_MAX_SECONDS=900 bash ./scripts/update_openclaw.sh + +# capture a timestamped run log file +bash ./scripts/update_openclaw.sh 2>&1 | tee "/tmp/openclaw-update-$(date +%Y%m%d-%H%M%S).log" +``` + +## Verify after upgrade + +```bash +openclaw --version +openclaw gateway status +openclaw gateway health --json +bash ./scripts/model_profile_switch.sh status +launchctl print gui/$(id -u)/ai.openclaw.local.model-schedule-guard | rg 'last exit code|state' +launchctl print gui/$(id -u)/ai.openclaw.local.model-budget-guard | rg 'last exit code|state' +ls -lT /tmp/openclaw-gateway.launchd.log /tmp/openclaw-gateway.launchd.err.log +``` + +## If gateway is loaded but not running + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +If still failing, see: +- `docs/operations/troubleshooting.md` diff --git a/openclaw-setup-max/docs/operations/troubleshooting.md b/openclaw-setup-max/docs/operations/troubleshooting.md index dd97317..cf26717 100644 --- a/openclaw-setup-max/docs/operations/troubleshooting.md +++ b/openclaw-setup-max/docs/operations/troubleshooting.md @@ -106,30 +106,30 @@ openclaw gateway run --force - Launchd reports exit code `78: EX_CONFIG`. ### Fix that worked -- Ensure LaunchAgent command includes `gateway run`. -- Keep logs in `/tmp` (avoids potential external-volume path issues). +- Preferred: run the upgrade workflow, which self-heals LaunchAgent + guards. +- `update_openclaw.sh` prints timestamped logs so restart timing is visible during the run. + +```bash +cd /Volumes/Data/openclaw-setups/openclaw-setup-max +bash ./scripts/update_openclaw.sh +``` + +- Manual fallback: + 1. Keep LaunchAgent logs in `/tmp` (avoids external-volume symlink path issues). + 2. Reinstall and restart the gateway service. File: - `~/Library/LaunchAgents/ai.openclaw.gateway.plist` -Expected ProgramArguments section: -```xml - - /opt/homebrew/bin/node - /opt/homebrew/lib/node_modules/openclaw/dist/index.js - gateway - run - --port - 18789 - +Set log paths: +```bash +/usr/libexec/PlistBuddy -c "Set :StandardOutPath /tmp/openclaw-gateway.launchd.log" ~/Library/LaunchAgents/ai.openclaw.gateway.plist +/usr/libexec/PlistBuddy -c "Set :StandardErrorPath /tmp/openclaw-gateway.launchd.err.log" ~/Library/LaunchAgents/ai.openclaw.gateway.plist ``` -Expected log paths: -- `/tmp/openclaw-gateway.launchd.log` -- `/tmp/openclaw-gateway.launchd.err.log` - Reload service: ```bash +openclaw gateway install --force launchctl bootout gui/$(id -u)/ai.openclaw.gateway 2>/dev/null || true launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway @@ -137,8 +137,10 @@ launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway Verify: ```bash -openclaw status --deep +launchctl print gui/$(id -u)/ai.openclaw.gateway | rg 'state =|last exit code|runs =' +openclaw gateway status openclaw gateway health --json +ls -lT /tmp/openclaw-gateway.launchd.log /tmp/openclaw-gateway.launchd.err.log ``` ## 6) docs/context/BOOT.md hook setup diff --git a/openclaw-setup-max/scripts/update_openclaw.sh b/openclaw-setup-max/scripts/update_openclaw.sh new file mode 100755 index 0000000..7f7306d --- /dev/null +++ b/openclaw-setup-max/scripts/update_openclaw.sh @@ -0,0 +1,365 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + bash ./scripts/update_openclaw.sh [options] + +Options: + --channel Update channel (default: stable) + --tag One-off npm tag/version (example: beta, 2026.2.24) + --skip-guard-stop Do not stop local model guard LaunchAgents before upgrade + --skip-guard-reinstall Do not reinstall local model guard LaunchAgents after upgrade + -h, --help Show this help + +Examples: + bash ./scripts/update_openclaw.sh + bash ./scripts/update_openclaw.sh --channel beta + bash ./scripts/update_openclaw.sh --tag 2026.2.24 +USAGE +} + +now_ts() { date '+%Y-%m-%d %H:%M:%S%z'; } +log() { printf '[%s] [update-openclaw] %s\n' "$(now_ts)" "$*"; } +warn() { printf '[%s] [update-openclaw] WARN: %s\n' "$(now_ts)" "$*" >&2; } +err() { printf '[%s] [update-openclaw] ERROR: %s\n' "$(now_ts)" "$*" >&2; } + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + err "Required command missing: $1" + exit 1 + fi +} + +run_with_timeout() { + local timeout_s="$1" + shift + + local start_ts elapsed pid + start_ts="$(date +%s)" + "$@" & + pid=$! + + while kill -0 "$pid" 2>/dev/null; do + elapsed="$(( $(date +%s) - start_ts ))" + if (( elapsed >= timeout_s )); then + warn "Command timed out after ${timeout_s}s: $*" + kill -INT "$pid" 2>/dev/null || true + sleep 2 + kill -TERM "$pid" 2>/dev/null || true + sleep 1 + kill -KILL "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + sleep 1 + done + + wait "$pid" +} + +extract_version() { + local raw + raw="$("$1" --version 2>&1 || true)" + printf '%s\n' "$raw" | grep -Eo '[0-9]{4}\.[0-9]+\.[0-9]+' | head -n1 +} + +print_guard_status() { + local label="$1" + launchctl print "gui/$(id -u)/$label" 2>/dev/null | \ + grep -E 'state =|last exit code|runs =|path =|program =' || true +} + +is_launchagent_loaded() { + local label="$1" + launchctl print "gui/$(id -u)/$label" >/dev/null 2>&1 +} + +is_gateway_launchagent_running() { + local label="$1" + launchctl print "gui/$(id -u)/$label" 2>/dev/null | grep -q 'state = running' +} + +fix_gateway_launchagent_log_paths_for_external_state() { + local label="$1" + local plist="$HOME/Library/LaunchAgents/$label.plist" + local state_dir_link_target="" + + if [[ ! -f "$plist" ]]; then + return 0 + fi + + # launchd may fail with EX_CONFIG when stdout/stderr paths point into + # ~/.openclaw on an external-volume symlink. Force local /tmp logs. + if [[ -L "$HOME/.openclaw" ]]; then + state_dir_link_target="$(readlink "$HOME/.openclaw" || true)" + fi + if [[ "$state_dir_link_target" != /Volumes/* ]]; then + return 0 + fi + + if ! command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then + warn "PlistBuddy not found; skipping LaunchAgent log-path patch." + return 0 + fi + + local desired_out="/tmp/openclaw-gateway.launchd.log" + local desired_err="/tmp/openclaw-gateway.launchd.err.log" + local current_out="" + local current_err="" + + current_out="$(/usr/libexec/PlistBuddy -c 'Print :StandardOutPath' "$plist" 2>/dev/null || true)" + current_err="$(/usr/libexec/PlistBuddy -c 'Print :StandardErrorPath' "$plist" 2>/dev/null || true)" + + if [[ "$current_out" == "$desired_out" && "$current_err" == "$desired_err" ]]; then + return 0 + fi + + log "Patching gateway LaunchAgent logs to local /tmp paths (external state dir detected)" + /usr/libexec/PlistBuddy -c "Set :StandardOutPath $desired_out" "$plist" + /usr/libexec/PlistBuddy -c "Set :StandardErrorPath $desired_err" "$plist" + + launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$plist" + launchctl kickstart -k "gui/$(id -u)/$label" +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +CHANNEL="stable" +TAG="" +STOP_GUARDS="true" +REINSTALL_GUARDS="true" +UPDATE_MAX_SECONDS="${UPDATE_MAX_SECONDS:-600}" + +while (( "$#" )); do + case "$1" in + --channel) + shift + if [[ $# -eq 0 ]]; then + err "--channel requires a value" + usage + exit 1 + fi + CHANNEL="$1" + ;; + --tag) + shift + if [[ $# -eq 0 ]]; then + err "--tag requires a value" + usage + exit 1 + fi + TAG="$1" + ;; + --skip-guard-stop) + STOP_GUARDS="false" + ;; + --skip-guard-reinstall) + REINSTALL_GUARDS="false" + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "Unknown argument: $1" + usage + exit 1 + ;; + esac + shift +done + +case "$CHANNEL" in + stable|beta|dev) ;; + *) + err "Invalid --channel '$CHANNEL'. Expected stable, beta, or dev." + exit 1 + ;; +esac + +if ! [[ "$UPDATE_MAX_SECONDS" =~ ^[0-9]+$ ]] || [[ "$UPDATE_MAX_SECONDS" -lt 1 ]]; then + err "UPDATE_MAX_SECONDS must be a positive integer." + exit 1 +fi + +require_cmd openclaw +require_cmd launchctl +require_cmd cp +require_cmd mkdir +require_cmd date + +if ! command -v npm >/dev/null 2>&1; then + warn "npm not found; fallback npm upgrade path will be unavailable." +fi + +ts="$(date +%Y%m%d-%H%M%S)" +backup_dir="$HOME/.openclaw/backups/update-$ts" +mkdir -p "$backup_dir" + +config_file="$HOME/.openclaw/openclaw.json" +sessions_file="$HOME/.openclaw/agents/main/sessions/sessions.json" + +log "Backing up OpenClaw files to: $backup_dir" +if [[ -f "$config_file" ]]; then + cp "$config_file" "$backup_dir/openclaw.json" +else + warn "Config not found: $config_file" +fi +if [[ -f "$sessions_file" ]]; then + cp "$sessions_file" "$backup_dir/sessions.json" +else + warn "Sessions file not found: $sessions_file" +fi + +if [[ "$STOP_GUARDS" == "true" ]]; then + log "Stopping local model guard LaunchAgents to avoid config races during update" + launchctl bootout "gui/$(id -u)/ai.openclaw.local.model-budget-guard" 2>/dev/null || true + launchctl bootout "gui/$(id -u)/ai.openclaw.local.model-schedule-guard" 2>/dev/null || true +fi + +log "Stopping OpenClaw gateway before update" +if ! openclaw gateway stop; then + warn "Gateway stop returned non-zero (it may already be stopped). Continuing." +fi + +current_version="$(extract_version openclaw || true)" +if [[ -n "$current_version" ]]; then + log "Current OpenClaw version: $current_version" +else + warn "Could not parse current OpenClaw version from 'openclaw --version'." +fi + +if command -v npm >/dev/null 2>&1; then + latest_npm_version="$(npm view openclaw version 2>/dev/null || true)" + if [[ -n "$latest_npm_version" ]]; then + log "Latest npm OpenClaw version: $latest_npm_version" + fi +fi + +log "Running doctor fix for schema/config migration" +if ! openclaw doctor --fix --non-interactive; then + warn "Doctor fix returned non-zero. Continuing with upgrade." +fi + +update_cmd=(openclaw update --yes --no-restart --channel "$CHANNEL") +if [[ -n "$TAG" ]]; then + update_cmd+=(--tag "$TAG") +fi + +log "Running OpenClaw updater: ${update_cmd[*]}" +if ! run_with_timeout "$UPDATE_MAX_SECONDS" "${update_cmd[@]}"; then + warn "Built-in updater failed." + if ! command -v npm >/dev/null 2>&1; then + err "npm not available for fallback update." + exit 1 + fi + + pkg="openclaw@latest" + if [[ -n "$TAG" ]]; then + pkg="openclaw@$TAG" + fi + + log "Falling back to npm global install: npm install -g $pkg" + npm install -g "$pkg" +fi + +new_version="$(extract_version openclaw || true)" +if [[ -n "$new_version" ]]; then + log "Updated OpenClaw version: $new_version" +else + warn "Could not parse updated version from 'openclaw --version'." +fi + +log "Starting OpenClaw gateway after update" +if ! openclaw gateway start; then + err "Gateway failed to start after update." + exit 1 +fi + +# Some OpenClaw versions return success from "gateway start" even when the +# LaunchAgent is not installed/loaded; enforce a service-managed gateway. +gateway_label="ai.openclaw.gateway" +if ! launchctl print "gui/$(id -u)/$gateway_label" >/dev/null 2>&1; then + warn "Gateway LaunchAgent is not loaded after start; installing and retrying." + if ! openclaw gateway install --force; then + err "Failed to install gateway service." + exit 1 + fi + if ! openclaw gateway start; then + err "Gateway failed to start after installing service." + exit 1 + fi + if ! launchctl print "gui/$(id -u)/$gateway_label" >/dev/null 2>&1; then + err "Gateway LaunchAgent is still not loaded after install/start." + exit 1 + fi +fi + +fix_gateway_launchagent_log_paths_for_external_state "$gateway_label" + +if ! is_gateway_launchagent_running "$gateway_label"; then + warn "Gateway LaunchAgent is loaded but not running; attempting kickstart." + launchctl kickstart -k "gui/$(id -u)/$gateway_label" || true + sleep 2 +fi +if ! is_gateway_launchagent_running "$gateway_label"; then + err "Gateway LaunchAgent is still not running." + launchctl print "gui/$(id -u)/$gateway_label" | grep -E 'state =|last exit code|runs =|path =|program =' || true + exit 1 +fi + +log "Running post-upgrade health checks" +openclaw doctor --non-interactive || true +openclaw gateway status || true +openclaw models status || true + +if [[ "$REINSTALL_GUARDS" == "true" ]]; then + installer="$SCRIPT_DIR/install_local_model_guardrails.sh" + if [[ -x "$installer" ]]; then + log "Reinstalling local model guard LaunchAgents" + bash "$installer" + + # Run both periodic guards immediately after reinstall so profile/budget + # state is refreshed in this update run (not only on the next interval). + launchctl kickstart -k "gui/$(id -u)/ai.openclaw.local.model-schedule-guard" 2>/dev/null || true + launchctl kickstart -k "gui/$(id -u)/ai.openclaw.local.model-budget-guard" 2>/dev/null || true + sleep 2 + + if ! is_launchagent_loaded "ai.openclaw.local.model-schedule-guard"; then + err "Schedule guard LaunchAgent is not loaded after reinstall." + exit 1 + fi + if ! is_launchagent_loaded "ai.openclaw.local.model-budget-guard"; then + err "Budget guard LaunchAgent is not loaded after reinstall." + exit 1 + fi + else + warn "Guard installer missing or not executable: $installer" + fi +fi + +log "Local guard status summary" +print_guard_status "ai.openclaw.local.model-budget-guard" +print_guard_status "ai.openclaw.local.model-schedule-guard" + +if [[ -x "$SCRIPT_DIR/model_profile_switch.sh" ]]; then + log "Profile status" + bash "$SCRIPT_DIR/model_profile_switch.sh" status || true +fi + +cat </dev/null | sed 's/^v//' | cut -d. -f1 } +fix_gateway_launchagent_logs_for_external_state() { + local plist="$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist" + local desired_out="/tmp/openclaw-gateway.launchd.log" + local desired_err="/tmp/openclaw-gateway.launchd.err.log" + + if [[ ! -L "$HOME/.openclaw" ]]; then + return 0 + fi + local state_dir_link_target + state_dir_link_target="$(readlink "$HOME/.openclaw" || true)" + if [[ "$state_dir_link_target" != /Volumes/* ]]; then + return 0 + fi + + if [[ ! -f "$plist" ]]; then + return 0 + fi + if ! command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then + log_warn "PlistBuddy not found; skipping gateway LaunchAgent log-path patch." + return 0 + fi + + local current_out current_err + current_out="$(/usr/libexec/PlistBuddy -c 'Print :StandardOutPath' "$plist" 2>/dev/null || true)" + current_err="$(/usr/libexec/PlistBuddy -c 'Print :StandardErrorPath' "$plist" 2>/dev/null || true)" + + if [[ "$current_out" == "$desired_out" && "$current_err" == "$desired_err" ]]; then + return 0 + fi + + log_warn "Patching gateway LaunchAgent logs to /tmp (external OpenClaw state dir detected)." + /usr/libexec/PlistBuddy -c "Set :StandardOutPath $desired_out" "$plist" + /usr/libexec/PlistBuddy -c "Set :StandardErrorPath $desired_err" "$plist" + + launchctl bootout "gui/$(id -u)/ai.openclaw.gateway" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$plist" 2>/dev/null || true + launchctl kickstart -k "gui/$(id -u)/ai.openclaw.gateway" 2>/dev/null || true +} + link_data_dir() { local source="$1" local target="$2" local label="$3" + # Safety: target must be a real directory on the external volume, not a symlink. + if [ -L "$target" ]; then + local target_link + target_link="$(readlink "$target" || true)" + log_err "ERROR: ${label} target is a symlink: ${target} -> ${target_link}" + log_err "Expected a real directory on external storage. Remove/fix target symlink and re-run." + exit 1 + fi + mkdir -p "$target" if [ -L "$source" ]; then @@ -150,6 +198,12 @@ link_data_dir() { ln -s "$target" "$source" log_info "${label}: symlink created ${source} -> ${target}" + + # Verify direction is exactly source -> target. + if [ ! -L "$source" ] || [ "$(readlink "$source")" != "$target" ]; then + log_err "ERROR: ${label} symlink direction check failed for ${source} -> ${target}" + exit 1 + fi } ensure_ollama_running() { @@ -201,6 +255,7 @@ fi mkdir -p "$OLLAMA_DATA_TARGET" "$OPENCLAW_DATA_TARGET" link_data_dir "$HOME/.ollama" "$OLLAMA_DATA_TARGET" "Ollama" link_data_dir "$HOME/.openclaw" "$OPENCLAW_DATA_TARGET" "OpenClaw" +fix_gateway_launchagent_logs_for_external_state # Step 2: Install / verify Ollama if ! command -v ollama >/dev/null 2>&1; then