* fix(watch): defer restart when dist/ is mid-rebuild (#99603)
Defer gateway:watch child restart when run-node's build/runtime-postbuild
readiness contract reports a hard dist/ failure. Only defers on conditions
where the child physically cannot start, preserving normal restart behavior
for soft staleness that run-node can rebuild on start.
Hard failures (defer restart):
- missing_dist_entry (mid-rebuild)
- missing_bundled_plugin_dist_entry
- missing_private_qa_dist
- missing_runtime_postbuild_output
Soft staleness (allow restart, run-node rebuilds):
- missing_build_stamp (w/ direct entry.js check for masked case)
- git_head_changed, dirty_watched_tree
- config_newer, build_stamp_missing_head, source_mtime_newer
isBuildReadyForRestart delegates to resolveBuildRequirement and
resolveRuntimePostBuildRequirement from run-node.mjs, using the
watcher process environment for consistency with the child runner.
200ms polling with 5-min timeout. Child-exit bounded recovery.
Clears stale restartRequested on timeout/poll errors.
Ref: #99603
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(watch): deduplicate deferral loops, add single-flight guard, remove lint suppression
- Extract deferUntilBuildReady(onReady, onTimeout) shared helper,
replacing the near-identical pollBuildReady and pollAfterChildExit.
- Add single-flight guard (deferredRestartActive) so concurrent file
change events during deferral coalesce into one poll loop instead of
spawning a git-subprocess storm.
- Replace inline .catch arrow callback with method-style call, removing
the oxlint-disable-next-line suppression and its allowlist entry.
- Add tests: coalescing multiple changes, requestRestart recovery.
Ref: #99603
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep gateway watch alive during concurrent builds
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The previous migration to tsdown was reverted because it caused a ~20x slowdown when running OpenClaw from the repo. @hyf0 investigated and found that simply renaming the `dist` folder also caused the same slowdown. It turns out the Plugin script loader has a bunch of voodoo vibe logic to determine if it should load files from source and compile them, or if it should load them from dist. When building with tsdown, the filesystem layout is different (bundled), and so some files weren't in the right location, and the Plugin script loader decided to compile source files from scratch using Jiti.
The new implementation uses tsdown to embed `NODE_ENV: 'production'`, which we now use to determine if we are running OpenClaw from a "production environmen" (ie. from dist). This removes the slop in favor of a deterministic toggle, and doesn't rely on directory names or similar.
There is some code reaching into `dist` to load specific modules, primarily in the voice-call extension, which I simplified into loading an "officially" exported `extensionAPI.js` file. With tsdown, entry points need to be explicitly configured, so we should be able to avoid sloppy code reaching into internals from now on. This might break some existing users, but if it does, it's because they were using "private" APIs.