A reachable gateway whose health check failed either escaped as a raw thrown
error or hit healthCommand's CLI-style runtime.exit(1), killing non-interactive
onboarding before logNonInteractiveOnboardingJson could emit the --json summary
and polluting JSON stdout with human diagnostic text. Route health failures
through logNonInteractiveOnboardingFailure (structured ok:false payload in
--json mode, framed text otherwise) via healthCommandNonExiting, and capture
healthCommand's human output off stdout in --json runs so the diagnostic lands
in the payload's detail field instead.
The first `openclaw doctor` on a freshly onboarded install reported "Persisted
plugin registry is missing or stale. Repair with `openclaw doctor --fix`".
Nothing was wrong: the `installed_plugin_index` row had never existed, `plugins
list` reported 148 plugins without it, no retired `plugins.installs` records
were present, and starting the gateway once builds the row by itself -- measured
going 0 -> 1 across a single `gateway run`.
`preflightPluginRegistryInstallMigration` returned a single `action: "migrate"`
whether the index was absent or unreadable, and the health issue name
`registry-missing-or-stale` shows the conflation: missing and stale are
different states and only one is a problem.
Split that into `initialize | migrate`. A root with no persisted index, no
install records, and no retired config records is initialization the gateway
owns, so doctor stays quiet. Install records without a readable index stays a
migration, as does a config still carrying retired `plugins.installs` records,
or a caller that supplied no config to prove otherwise. `doctor --fix` still
builds the index in every case -- this changes the warning, not the repair.
Production +5 LOC.
Host zlib versions compress identical startup assets into different bytes, causing false Linux budget failures. Emit canonical shipped pako gzip sidecars, restore the 512 B ratchet tolerance, and lower the startup baseline from 348351 B to 344531 B.
* fix(wizard): keep embedded health-check failures visible instead of exiting mid-flow
healthCommand's reachable-gateway auth diagnostic paths call runtime.exit(1),
which with defaultRuntime hard-kills the hosting configure wizard, onboarding
finalize, or doctor daemon flow mid-render — dropping the failure framing,
docs guidance, and outro. Add healthCommandNonExiting, which traps that
CLI-style exit into ExitError so the host flow owns the outcome, and use it
at every embedded call site.
Also fix the doctor e2e harness createConfigIO mock missing configPath, which
broke doctor.runs-legacy-state-migrations on current main.
* fix(onboard): reflect a failed health check in the finalize outro
A reachable gateway whose health check failed still ended onboarding with the
plain success outro because completion gating only read the earlier
reachability probe. Record the health outcome as its own fact and end with a
dedicated outro pointing at openclaw health. (ClawSweeper P1 on #126758.)
`openclaw onboard --openai-api-key 'openclaw onboard --auth-choice ...'`
correctly refuses with "Paste the API key value, not an OpenClaw onboarding
command", exits 1, and writes no config. The identical value supplied through
`OPENAI_API_KEY` -- the form `docs/start/wizard-cli-automation.md` documents for
automation -- exited 0 with empty stderr and persisted the command string as the
credential.
`isMalformedApiKeyInput` was already imported into this file but guarded only
the `flagKey` branch; both `resolveEnvKey()` branches returned unchecked. The
operator finished onboarding believing they were configured, and nothing told
them otherwise until an agent turn failed or they happened to run
`openclaw doctor`, which classifies that exact value as `malformed_api_key` and
prints the hint they never saw.
Route every operator-supplied key -- flag, env, and secret-ref env -- through one
guard, and name the environment variable in the message so an operator with
several exported keys knows which one is wrong. The stored-profile branch stays
unguarded on purpose: a bad key already on disk is doctor's to diagnose, and
refusing there would strand someone re-onboarding to replace it.
`openclaw onboard` refuses a corrupt openclaw.json and tells the operator to run
`openclaw doctor --fix`. Doctor then answered with one sentence -- "Config could
not be parsed or recovered ... refusing to apply repairs" -- named no next step,
and exited 1. The operator was left looping between two commands that pointed at
each other.
The refuse path also wrote openclaw.json.clobbered.<timestamp> and called it
"Original preserved", but it had not clobbered anything: at that point the
snapshot is a reread of the live file, so the copy was byte-identical to the
untouched config. Three failed runs left three identical copies.
Drop the copy and say what to do instead: name the file, state that it cannot be
repaired automatically, and point at `openclaw config validate` for the exact
parse position, hand-editing, or moving the file aside and re-running
`openclaw onboard`. Commands go through formatCliCommand so profile and
container invocations stay pasteable.
`doctor-config-preflight.ts` was the only caller of the public
preserveConfigSnapshotAsClobbered wrapper, so the wrapper, its factory entry and
its barrel export go too; the genuine recovery paths keep using the core helper
and still preserve real originals. Production -16 LOC.