* feat(gateway): recover channels and health promptly after host sleep A dependency-free thaw detector rides the existing 30s maintenance tick: when the process resumes after being frozen >=45s beyond cadence (laptop sleep, VM pause, SIGSTOP), the gateway restarts running channel accounts (dead sockets otherwise take up to ~35 minutes to notice), refreshes health/presence, and resets the event-loop histogram so the freeze does not read as degradation. Admission is rechecked before every recovery side effect; a suspension beginning mid-recovery re-pends the thaw, and timed-out channel stops complete their two-call restart in one pass. The macOS app cooperates: NSWorkspace sleep/wake observers in GatewayConnectivityCoordinator best-effort prepare a local gateway suspension before sleep and resume it on wake, never blocking sleep. The lease is bound to the route that prepared it and always cleared on wake; route or mode changes across sleep drop it to self-expiry. Live proof: SIGSTOP 85s on an isolated dev gateway -> 'host thaw detected: process was frozen ~57683ms', channels restarted, health ok, eventLoop degraded=false after thaw. * fix(macos): resume a sleep lease whose prepare response arrives after wake A prepare completing after didWake previously discarded the lease id, fencing the gateway until the two-minute expiry after micro-sleeps; the late response now resumes immediately. Document the conservative route-token drift tradeoff. * fix(macos): retry wake resume after refreshing the dead post-sleep transport After real sleep the WebSocket is usually dead exactly when resume runs; refresh the endpoint first, then attempt resume up to three times with bounded delays, clearing the lease only on success or exhaustion. A new sleep cycle aborts in-flight retries. * fix(gateway): bound plugin stopAccount so channel stops cannot wedge recovery stopChannel awaited plugin stopAccount unbounded; a never-settling stop hung the thaw restart (and health-monitor sweeps) and held the single-flight recovery guard forever. Race it against the existing 5s stop timeout; the timed-out path flows into the established recoveryStopTimedOut two-call restart contract. Regression wedges pre-fix. * refactor(gateway): move thaw channel restart off ChannelManager and fence mid-pass restartRunningChannelAccounts is a standalone helper over the public manager surface with a shouldContinue probe checked before every stop and start, so a suspension committing while an account stop is awaited leaves later accounts untouched. Regression covers the mid-pass close. * fix(gateway): sanitize late writes from an abandoned stopAccount An abandoned (timed-out) stopAccount can settle after its replacement started; route its late setStatus writes through the existing stale-task sanitizer so they cannot repaint or tear down the replacement. Regression fails pre-fix.
OpenClaw macOS app (dev + signing)
Quick dev run
# from repo root
scripts/restart-mac.sh
Options:
scripts/restart-mac.sh --no-sign # fastest dev; ad-hoc signing (TCC permissions do not stick)
scripts/restart-mac.sh --sign # force code signing (requires cert)
scripts/restart-mac.sh --background-only # keep services running without automatic windows
--background-only suppresses first-run onboarding, update and CLI prompts, and
the --chat/--dashboard auto-open helpers. Pairing, control-channel, and Mac
node services still start. Combine it with --attach-only when an external
process owns the local Gateway.
App profiles
Launch a fully isolated app instance with the same profile name used by the CLI:
OPENCLAW_PROFILE=work /Applications/OpenClaw.app/Contents/MacOS/OpenClaw
Profile names use 1–64 lowercase letters, numbers, underscores, or hyphens and
must start with a letter or number. default selects the normal app; gateway,
mac, and node are reserved LaunchAgent identities.
scripts/restart-mac.sh intentionally rejects named profiles because its
packaging cleanup is host-global. Build/package normally, then launch the named
profile directly with the command above.
A named profile keeps state in ~/.openclaw-<name>, uses its own app defaults,
Keychain services, duplicate-instance lock, and the CLI-managed Gateway service
ai.openclaw.<name>. Unless config or environment selects a port, each profile
derives a stable port in the profile 20000...59999 range. The app does not
install or modify the host-global Mac node
service or OpenClaw login item while a profile is active. The runtime child node
still runs in process as usual. App relocation, Sparkle updates, and post-update
service repair are disabled in profile mode; update the installed app through
the normal default-profile workflow.
Packaging flow
scripts/package-mac-app.sh
Creates dist/OpenClaw.app and signs it via scripts/codesign-mac-app.sh.
Signing behavior
Auto-selects identity (first match):
- Developer ID Application
- Apple Distribution
- Apple Development
- first available identity
If none found:
- errors by default
- set
ALLOW_ADHOC_SIGNING=1orSIGN_IDENTITY="-"to ad-hoc sign
Team ID audit (Sparkle mismatch guard)
After signing, we read the app bundle Team ID and compare every Mach-O inside the app. If any embedded binary has a different Team ID, signing fails.
Skip the audit:
SKIP_TEAM_ID_CHECK=1 scripts/package-mac-app.sh
Library validation workaround (dev only)
If Sparkle Team ID mismatch blocks loading (common with Apple Development certs), opt in:
DISABLE_LIBRARY_VALIDATION=1 scripts/package-mac-app.sh
This adds com.apple.security.cs.disable-library-validation to app entitlements.
Use for local dev only; keep off for release builds.
Useful env flags
SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"ALLOW_ADHOC_SIGNING=1(ad-hoc, TCC permissions do not persist)CODESIGN_TIMESTAMP=off(offline debug)DISABLE_LIBRARY_VALIDATION=1(dev-only Sparkle workaround)SKIP_TEAM_ID_CHECK=1(bypass audit)