Skip to content

fix(cli): exit cleanly on startup migration refusal#110207

Merged
steipete merged 1 commit into
openclaw:mainfrom
VACInc:fix/startup-migration-clean-exit-109831
Jul 18, 2026
Merged

fix(cli): exit cleanly on startup migration refusal#110207
steipete merged 1 commit into
openclaw:mainfrom
VACInc:fix/startup-migration-clean-exit-109831

Conversation

@VACInc

@VACInc VACInc commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • report the three planned startup migration/config/plugin refusal reasons at their owner, then throw ExitError(1) so ensureConfigReady uses its existing typed runtime.exit path
  • add a hermetic child-process regression that exercises a real conflicting plugin-state migration, requires exit status 1 with no signal, verifies the refusal appears exactly once, and confirms the startup migration lease is released
  • audit the core DatabaseSync migration/checkpoint paths for throw-time handle leaks; every direct handle is already closed by finally, so no unrelated migration refactor is included

This PR was originally stacked on #110200. That prerequisite has now merged as 748fc044192a1c98d8344ea6682a3f38dd7ef384; the branch was rebased onto origin/main at 82fa956d5a22c65d0dd9f2bfc5ff964e45c9728e, and the already-applied ce6e200aaa2 commit dropped from this PR's history. The landing dependency is satisfied.

Refs #109831.

What Problem This Solves

Startup migration, invalid-config, and plugin-verification refusals were ordinary Errors. They bypassed the typed exit handling in src/cli/program/config-guard.ts, reached the generic src/entry.ts failure handler, and ended in its direct process.exit(1) path. In the reported supervised deployment, that path terminated with signal 11 / status 139 instead of a clean refusal status, turning a fail-closed migration result into a supervisor crash loop.

Root Cause

The proven pre-fix control flow is:

  1. src/commands/doctor-config-preflight.ts formatted an intentional readiness refusal but threw a plain Error.
  2. ensureConfigReady only converts ExitError into runtime.exit(error.code), so it rethrew the plain refusal.
  3. src/entry.ts added Could not start the CLI. / Reason: formatting and called process.exit(1).

The preflight's existing finally already releases its heartbeat and startup migration lease before the error leaves the owner. The fix preserves that unwind and hands the intentional refusal to the typed caller contract instead of treating it as an unexpected CLI startup failure.

The reporter's exact native signal source is not established without their core dump. This change fixes the incorrect refusal path and locks down the observable process contract; it does not claim to identify or patch the native module.

Why This Change Was Made

The refusal owner must print the reason because the typed ExitError branch intentionally does not format errors, while the generic entry catch is no longer reached. Printing once immediately before throwing ExitError(1) preserves the complete operator guidance without duplicating the generic wrapper.

This is the narrow owner-boundary fix: unexpected preflight errors still use the generic failure handler, and gateway readiness remains fail-closed.

The core migration audit covered the direct DatabaseSync constructors in src/infra/state-migrations.debug-proxy.ts, src/infra/state-migrations.task-sidecar-rows.ts, and src/infra/state-migrations.storage.ts, plus the checkpoint database wrapper used by src/infra/startup-migration-checkpoint.ts. Each closes in finally, including throw paths. No plugin-owned code was changed.

User Impact

When a startup migration gate refuses readiness, operators retain the full actionable refusal text exactly once. The CLI exits through the typed status-1 path, the migration lease is released, and supervisors see a normal non-zero exit rather than a signal in the covered regression environment.

Evidence

Rebase result

After #110200 merged, git fetch origin main and git rebase origin/main skipped its already-applied ce6e200aaa2 commit and replayed only this PR's fix. Main advanced once during the proof window, so the branch was rebased a second time before the final proof.

git log --oneline origin/main..HEAD on the published branch contains one commit:

d0281e2cc2a fix(cli): exit cleanly on migration refusal

Final identities:

origin/main=82fa956d5a22c65d0dd9f2bfc5ff964e45c9728e
head=d0281e2cc2a852f2c20903275fa7fa74ac996c92

Real behavior proof — exact rebased head

Environment: Linux source checkout, Node v25.9.0, actual src/entry.ts, no provider/channel credentials, and an isolated realpathed temporary HOME, config, and OPENCLAW_STATE_DIR. The canonical plugin-state row had timestamp 2000 and value {"version":1}; the legacy sidecar held the same key with newer timestamp 3000 and divergent value {"version":2}. That is intentionally unresolved under the merged #109832 migration contract.

A Node supervisor wrapper used spawnSync with a 60-second timeout and captured the wait status and signal separately. It launched the actual gateway entry twice against the same unresolved state:

node --import tsx src/entry.ts gateway run --allow-unconfigured

Observed on exact head d0281e2cc2a852f2c20903275fa7fa74ac996c92:

gateway_run_1 status=1 signal=none refusal_count=1 stale_lease_count=0 generic_wrapper_count=0
after_gateway_run_1 active_startup_migration_lease=false
gateway_run_2 status=1 signal=none refusal_count=1 stale_lease_count=0 generic_wrapper_count=0
after_gateway_run_2 active_startup_migration_lease=false

Redacted stderr from the first run:

[state-migrations] Legacy state migration warnings:
- Left plugin-state sidecar in place because 1 row differs from shared state without a newer canonical timestamp. First key: proof-plugin/proof-namespace/proof-key
OpenClaw startup migrations did not complete cleanly; refusing to report the gateway ready.
- Left plugin-state sidecar in place because 1 row differs from shared state without a newer canonical timestamp. First key: proof-plugin/proof-namespace/proof-key
Run "openclaw doctor --fix" against the mounted state/config, then restart the container.

This proves both supervisor-observed gateway attempts exited with code 1 rather than a signal/status 139, printed the refusal headline exactly once on stderr, omitted the generic outer-entry wrapper, and released the startup migration lease. The second run acquired a fresh lease and did not report a stale/active lease.

Doctor-path parity

The same exact-head wrapper and unresolved fixture then ran:

node --import tsx src/entry.ts doctor --fix --non-interactive --yes

Observed:

doctor_fix status=0 signal=none stale_lease_count=0 active_startup_migration_lease=false

Doctor continues to report the unresolved migration without gating gateway readiness, exits normally, and releases its lease. Gateway remains fail-closed on the next startup attempt.

Guard-boundary verification

The existing ensureConfigReady catch in src/cli/program/config-guard.ts intercepts ExitError and calls runtime.exit(error.code). The live process transcript proves the refusal reaches that branch: the generic [openclaw] Could not start the CLI. wrapper count is zero, while the refusal owner prints its reason exactly once.

Post-rebase fixture correction

The first focused run after rebasing failed because merged #109832 correctly archived the old fixture's older sidecar row; the child then continued to a locally occupied gateway port. That was a stale test premise, not a product regression. The fixture now uses a newer divergent sidecar timestamp, which is the migration state that must remain unresolved. The corrected exact-head run is green.

Mandatory uncommitted autoreview of that fixture correction reported:

autoreview clean: no accepted/actionable findings reported
overall: patch is correct (0.98)

Pre-fix contrast and native-crash boundary

An earlier run from pre-fix merge base 433b3ce86d71a88fb34d4f61f6f10c36b912aeee exited status 1 with no signal on this host, but included generic_wrapper_count=1; the patched path has generic_wrapper_count=0. The reporter's SIGSEGV did not reproduce in this environment on either revision. This proof establishes the clean typed-exit contract owned by this PR; identifying the reporter's crashing native module remains deferred pending their core dump.

Verification

pnpm format src/commands/doctor-config-preflight.process.test.ts
Finished in 4ms on 1 files using 1 threads.

node scripts/run-vitest.mjs src/commands/doctor-config-preflight.state-migration.test.ts src/commands/doctor-config-preflight.process.test.ts
Test Files  2 passed (2)
Tests       24 passed (24)
Duration    2.51s
[test] passed 1 Vitest shard in 4.62s

git diff --check origin/main...HEAD
passed

What Was Not Tested

  • The reporter's exact container/supervisor/native-module failure was not reproduced; identifying the crashing native module requires the reporter's core dump and is intentionally deferred.
  • No successful full gateway channel/provider startup was exercised. The real child process stops at the intended preflight refusal, before runGatewayCommand starts runtime services.
  • No broad suite was run; focused coverage exercises the changed refusal owner, existing migration cases, child-process exit semantics, lease cleanup, and the scoped core changed gate.

@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes commands Command implementations size: M labels Jul 17, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jul 17, 2026
@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 17, 2026, 8:29 PM ET / July 18, 2026, 00:29 UTC.

Summary
The branch reports intentional startup migration, invalid-config, and plugin-verification refusals at their owner, exits through ExitError(1), and adds a real child-process regression for migration-conflict exit and lease cleanup.

PR surface: Source +9, Tests +122. Total +131 across 2 files.

Reproducibility: yes. at source level: the added hermetic child process creates a real unresolved plugin-state conflict and verifies the gateway exit status, output cardinality, and lease cleanup. The reporter’s exact native SIGSEGV environment was not reproduced.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: database schema: src/commands/doctor-config-preflight.process.test.ts, migration/backfill/repair: src/commands/doctor-config-preflight.process.test.ts, migration/backfill/repair: src/commands/doctor-config-preflight.ts, unknown-data-model-change: src/commands/doctor-config-preflight.process.test.ts, unknown-data-model-change: src/commands/doctor-config-preflight.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Rebase onto current main and repeat the focused process regression on the refreshed head.

Risk before merge

  • [P1] The branch is behind current main; refresh it and rerun the focused child-process proof so the ExitError/config-guard behavior is validated against the merge head.

Maintainer options:

  1. Refresh and revalidate the startup path (recommended)
    Rebase onto current main and rerun the child-process migration-conflict proof before merge so the intended clean-exit behavior remains valid at the actual merge head.

Next step before merge

  • [P2] No discrete automated repair is supported; a maintainer should review the refreshed cleanly mergeable head and its focused proof.

Security
Cleared: The diff is limited to CLI refusal handling and a local SQLite-backed regression fixture; it adds no dependency, workflow, permission, secret, or supply-chain surface.

Review details

Best possible solution:

Keep the owner-side typed-exit design, rebase onto current main, and merge once the focused process regression and required checks still pass on the refreshed head.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: the added hermetic child process creates a real unresolved plugin-state conflict and verifies the gateway exit status, output cardinality, and lease cleanup. The reporter’s exact native SIGSEGV environment was not reproduced.

Is this the best way to solve the issue?

Yes: routing deliberate readiness refusals through the existing typed exit contract at their owner preserves actionable output while leaving unexpected preflight errors on the generic entry failure path.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against dc0285366efc.

Label changes

Label justifications:

  • P1: The PR addresses a gateway startup refusal path that can turn a recoverable operator action into a supervised crash loop.
  • merge-risk: 🚨 availability: It changes the process-termination path used when gateway startup deliberately refuses readiness, so a merge-head process proof is important.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR provides exact-head terminal evidence from real gateway and doctor child processes, including observed exit status, signal absence, diagnostic output, and migration-lease cleanup.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR provides exact-head terminal evidence from real gateway and doctor child processes, including observed exit status, signal absence, diagnostic output, and migration-lease cleanup.
Evidence reviewed

PR surface:

Source +9, Tests +122. Total +131 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 12 3 +9
Tests 1 122 0 +122
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 134 3 +131

What I checked:

  • Focused refusal-path change: The proposed helper emits the owner diagnostic and throws ExitError(1); the three planned readiness-refusal branches use it, leaving unexpected preflight errors on the generic failure path. (src/commands/doctor-config-preflight.ts:219, d0281e2cc2a8)
  • Real process proof: The PR body records two real src/entry.ts gateway run --allow-unconfigured child-process attempts on the exact head, each with status 1, no signal, one refusal, no generic wrapper, and no retained startup-migration lease; the focused regression suite reports 24 passing tests. (src/commands/doctor-config-preflight.process.test.ts:1, d0281e2cc2a8)
  • Adjacent merged prerequisite: The related diagnostic-classification PR was merged as 748fc044192a1c98d8344ea6682a3f38dd7ef384; this branch was rebased after that change and retains only the clean-exit fix. (src/commands/doctor-config-preflight.ts:483, 748fc044192a)
  • Current review state: GitHub reports the PR cleanly mergeable but behind main; the proof was run against base 82fa956d5a22c65d0dd9f2bfc5ff964e45c9728e, while the review target main is dc0285366efcfd3130f75349af030fdde3a86dcb. (dc0285366efc)

Likely related people:

  • VACInc: Authored the merged adjacent startup-preflight diagnostic work and the current focused clean-exit change in the same owner module. (role: recent area contributor; confidence: medium; commits: 748fc044192a, d0281e2cc2a8; files: src/commands/doctor-config-preflight.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (3 earlier review cycles)
  • reviewed 2026-07-17T22:29:19.807Z sha fd057e7 :: needs real behavior proof before merge. :: [P1] Handle ExitError before it reaches the generic entry catch
  • reviewed 2026-07-17T23:05:18.883Z sha fd057e7 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-17T23:46:53.294Z sha d0281e2 :: needs maintainer review before merge. :: none

@VACInc

VACInc commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 17, 2026
@VACInc
VACInc force-pushed the fix/startup-migration-clean-exit-109831 branch from fd057e7 to 5449ab1 Compare July 17, 2026 23:42
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed cli CLI command changes size: M labels Jul 17, 2026
@VACInc
VACInc force-pushed the fix/startup-migration-clean-exit-109831 branch from 5449ab1 to d0281e2 Compare July 17, 2026 23:44
@VACInc

VACInc commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@steipete steipete self-assigned this Jul 18, 2026
@steipete
steipete merged commit c08ce42 into openclaw:main Jul 18, 2026
120 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants