* fix(daemon): allow launchd restart while gateway port is busy
* fix(daemon): verify launchd restart port ownership
* fix(macos): refresh launchd restart ownership guard
Protect the managed LaunchAgent PID, re-read after cleanup, and reject mixed or unattributed port ownership before service mutation.
Co-authored-by: Shawn Duggan <shawn.duggan@gmail.com>
* fix(macos): refresh launchd pid during stale cleanup
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(doctor): bound legacy launchd cleanup
Bound both legacy launchctl cleanup attempts to five seconds while keeping the existing bootout-then-unload order. Only move the legacy plist after a successful command or an explicit already-unloaded result; leave it in place when timeout or another failure prevents confirmation so a later doctor run can retry. Report filesystem cleanup failures as skipped instead of claiming removal.
* fix(doctor): verify launchd cleanup state before moving plist
Poll a bounded launchctl print postcondition after legacy bootout and unload. Treat loaded, unknown, and timed-out probes as unconfirmed so doctor leaves the plist available for retry.
* test(doctor): clarify launchd cleanup postcondition
* fix(doctor): reject timed-out launchd probes
Preserve timeout evidence from the command runner, including sanitized timeout messages, so partial not-loaded output cannot authorize plist removal.
* fix(doctor): classify no-output launchd timeouts
* test(doctor): trim launchd cleanup cases
Co-authored-by: Alix-007 <li.long15@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(daemon): reload handoff outwaits gateway drain before re-bootstrapping
The launchd reload handoff waited only 15 x 0.2s (~3s) after bootout for the
label to unload, but the booted-out gateway keeps the label registered until
its drain-before-exit window (up to 300s) completes. Bootstrap then failed
with EIO mid-drain, and the kickstart -k fallback cannot succeed on a
booted-out label, leaving the LaunchAgent deregistered and the gateway down
until a manual bootstrap.
Extend the post-bootout wait to cover the full restart-deferral budget plus
margin (315 x 1s, derived from DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS), and
replace the single dead kickstart fallback with a bootstrap retry loop that
only falls back to kickstart -k while the label is actually registered, so
the handoff never exits with the service deregistered.
Closes#110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(daemon): scale reload bootout wait to the effective drain budget
A config-raised gateway.reload.deferralTimeoutMs would outlast the fixed
default-derived wait and reopen the stranded-LaunchAgent race. Thread the
effective deferral timeout from restartLaunchAgent into the handoff and derive
the reload bootout wait from it; unbounded (<=0) configs keep the finite
default wait, with the bootstrap retry loop covering the overshoot.
Related: #110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(daemon): derive reload wait from launchd ExitTimeOut and keep failed bootstrap status
Review follow-up: the bootout SIGTERM path is bounded by the plist's
ExitTimeOut, not the gateway's restart-deferral config, so derive the reload
bootout wait from LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS plus margin and drop the
deferralTimeoutMs threading. Capture a failed bootstrap's status in the else
branch: after a completed if with a false condition $? is 0, so exhausted
retries logged 'restart done' and exit 0 while the LaunchAgent stayed
deregistered. Adds an execution-level retry-exhaustion test that runs the
generated script against an always-failing launchctl stub.
Related: #110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(daemon): exercise delayed launchd reload handoff
Run the generated reload handoff through delayed-stop and exhausted-bootstrap
paths, and clarify that the wait covers launchd's ExitTimeOut stop window.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* test(daemon): narrow generated handoff script
Fail clearly when the spawn arguments omit the generated script and pass a
narrowed string to the execution helper.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* fix(daemon): retry bootstrap after launchd kickstart race
Continue the bootstrap retry loop when a label disappears between the
registration check and kickstart, and cover the race by executing the generated
handoff script.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* test(daemon): make handoff no-wait sentinel explicit
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
---------
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(onboarding): recommend plugins and skills from installed apps
Scan installed macOS apps during classic onboarding (TCC-free), gather
candidates from official catalogs + ClawHub search, let the configured
model pick genuine matches, and offer an opt-in multiselect install step.
Adds a device.apps node-host command (default-off sharing, Android-parity
envelope) so remote gateways can request a paired Mac's inventory, and a
wizard.appRecommendations kill switch. Custom setup-inference completions
no longer inherit the 32-token verification-probe output cap.
* feat(onboarding): recommend apps in guided flow
* fix(onboarding): harden app recommendations against ClawHub self-promotion
Third-party ClawHub skills are never pre-selected regardless of model tier
(publisher-controlled listing text reaches the matcher prompt and could
promote itself); their labels now say they install third-party code.
Installed-app scans follow symlinked .app bundles. Matcher output stays
bounded by the resolved model's own maxTokens budget (documented invariant).
* fix(onboarding): key official catalog candidates by resolved plugin id
Real catalog entries are package manifests without a top-level id; keying the
candidate map and channel/provider classification by entry.id collapsed the
whole official catalog into one undefined-keyed entry, so no official plugin
or channel was ever recommended. Regression test runs against the bundled
catalogs.
* fix(onboarding): satisfy lint, types, deadcode, and migration gates
Split the guided-onboarding test into a self-contained custodian suite to stay
under max-lines. Narrow app-recommendation exports (drop dead node-payload
normalizer, unexport internal types/helpers, route candidate tests through the
public API), replace map-spread with a helper, unexport device.apps result
types, add installedAppsSharing to node-host migration expectations, cast the
wizard multiselect mock, and regenerate the docs map.
* test(onboarding): register new live test in the shard classifier
* [AI] fix(daemon): set default --max-old-space-size=8192 in gateway service env
Generate a managed NODE_OPTIONS with --max-old-space-size=8192 for the
gateway systemd service, discarding ambient host NODE_OPTIONS to prevent
--require/--inspect persistence. 8192 MB is chosen as a safe default
that prevents the common OOM crash-loop with the default Node ~4 GB heap
under sustained load.
Related to #96203
* fix(daemon): adapt gateway heap limit to host memory
* fix(daemon): keep gateway heap helpers internal
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(kill-tree): verify process group leader before group kill to prevent gateway SIGTERM (#76259)
- Add isProcessGroupLeader() to killProcessTree/signalProcessTree: ps -p <pid> -o pgid= primary check with /proc/<pid>/stat fallback on Linux. Group kill only when the PID is its own process group leader; non-leaders fall back to single-pid kill, preventing accidental gateway SIGTERM when a non-detached child shares the gateway's process group.
- Propagate detached: true to all detached-spawn cleanup callers (exec-termination, agent-bundle LSP, mcp-stdio, bash, supervisor pty, agent-core nodejs) so detached group cleanup survives leader exit.
- Gateway/daemon cleanup paths (schtasks, restart-health) keep the leader-checked default (detached omitted).
Closes#76259
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(process): tighten process-group ownership checks
* refactor(daemon): split restart diagnostics
* refactor(daemon): isolate restart health types
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(daemon): write Windows gateway launchers in encodings wscript/cmd can decode
gateway.vbs and gateway.cmd were written as UTF-8 without BOM, but
wscript.exe only reads .vbs as ANSI or UTF-16 LE with BOM and cmd.exe
reads .cmd in the console OEM code page, so installs under CJK profile
paths failed with "file not found" (#107416).
Write .vbs as UTF-16 LE with BOM, write non-ASCII .cmd content in the
system code page when it matches the console page (CJK/Thai locales),
and BOM-sniff plus code-page-fallback on read so launchers from older
installs keep parsing and migrate on refresh. The hidden .vbs launch
path originates from #95480, which addressed console visibility only.
* refactor(daemon): drop unused WindowsLauncherScriptFormat export
The type is only referenced by encodeWindowsLauncherScript's format
parameter within the module, so the export tripped check-deadcode-exports.
Keep it module-local.
* fix(daemon): mark code-page cmd launchers with their encoding for deterministic readback
Prepend an ASCII '@rem openclaw-launcher-encoding=<label>' line to code-page
.cmd launchers and decode by that marker instead of sniffing UTF-8. Some GBK
byte sequences are valid UTF-8 (隆 = C2 A1 reads as ¡), so the old sniff
silently corrupted readback and rejected valid paths; the marker makes decode
deterministic and drops the code-page probe (a PowerShell spawn) from the
frequent readScheduledTaskCommand poll path.
Also fix the representability guard for euc-kr: Node ICU decodes euc-kr as
KS X 1001 only, but Windows code page 949 is cp949/UHC, so the TextDecoder
cross-check false-rejected ~8,800 UHC extension syllables (똠 = 8C 63) that
iconv encodes and cmd.exe reads fine. Verify euc-kr via iconv's own cp949
round-trip; keep TextDecoder for the other five labels.
* fix(infra): write Windows restart helper scripts through the launcher encoder
The update-time restart helper wrote its temp .cmd as raw UTF-8 while
embedding the restart-log path, task name, and task script path, so a CJK
profile path or task name broke the same way as the gateway launchers
(#107416). Route the write through encodeWindowsLauncherScript: ASCII content
stays byte-identical UTF-8, CJK content gets the marked code-page encoding, and
an unrepresentable task name now fails the restart attempt cleanly instead of
writing a script cmd.exe would misread.
* chore(deps): minimize pnpm-lock delta for the iconv-lite promotion
Reset pnpm-lock.yaml to origin/main and re-add only the iconv-lite root
importer entry, dropping the unrelated @types/node peer-context flips and
audio-decode deprecation metadata that a mismatched-toolchain regeneration had
pulled in. The diff vs main is now the three-line importer entry only; the
version already resolves in main's tree via express -> body-parser/raw-body.
* refactor(windows): centralize launcher encoding
Co-authored-by: Jason Yao <wsyjh8@gmail.com>
* style(windows): format launcher encoding test
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* refactor: delete dead infra and config exports
* refactor: preserve live infra and config contracts
* refactor(config): remove obsolete file-store lifecycle APIs
* refactor(infra): finish current-main dead export cleanup
* refactor(deadcode): trim auto-reply and CLI exports
* refactor(deadcode): trim cron and task exports
* refactor(deadcode): trim fleet and process exports
* test(deadcode): exercise live task and process seams
* test(fleet): cover stream redaction through owner module
* refactor(security): trim dead internal exports
* refactor(secrets): trim dead internal exports
* refactor(deadcode): trim remaining src exports
* refactor(deadcode): remove test-only runtime exports
* refactor(deadcode): trim pairing test exports
* refactor(deadcode): reconcile refreshed baseline
* test(auto-reply): deduplicate queue state imports
* fix(core): make indexed access explicit in auto-reply, infra, and config
Part 1/3 of the src NUIA phase-3b burn-down (#104600): iteration and
destructuring over index reads, boundary guards on parsed input, and
named invariants. Config path walkers bind the path head once; SQLite
migration key handling is hoisted without query-shape changes.
* fix(core): make indexed access explicit in cli, gateway, commands, security, shared
Part 2/3: argv/token selection restructured, gateway event/attachment
invariants named, security parsers stay fail-closed (invariant
violations throw), edit-distance matrices access checked entries.
* fix(core): make indexed access explicit across remaining src surfaces
Part 3/3: channels, plugins, process, cron, plugin-sdk, media, logging,
tui, hooks, daemon, and small directories. Latent bug fixed: a tailnet
resolver could leak undefined through a string|null contract and now
fails with a descriptive local error.
* fix(core): keep optional boundaries optional after per-commit review
Review findings: expectDefined misused where absence is a legitimate
state. CLI --profile/route-args missing next tokens take their existing
miss paths; help normalization compares --help against the last
positional again; first-time plugin install spreads absent cfg.plugins;
denylist scan iterates manifest dependency entries instead of throwing
on omitted sections; tailnet resolver returns a guaranteed string at
the source instead of a caller-side undefined throw.
* refactor(core): closed-key provider labels and honest optional passthroughs
PROVIDER_LABELS becomes a satisfies-typed closed record (static reads
provably defined; dynamic lookups go through providerUsageLabel with
honest string|undefined). Status-scan overview passes its optional
params through unchanged instead of asserting them.
* fix(channels): make getChatChannelMeta honestly optional
The original signature claimed ChatChannelMeta while leaking undefined
on bundled channel id metadata drift; three of four callers already
handled absence. The return type now says so, and the one assuming
caller falls back to the raw channel label.
* fix(core): index-safety for post-rebase main drift
Covers the sqlite-sessions flip and auth-source-plan code that landed
mid-phase, plus the channel-validation test consuming the now honestly
optional getChatChannelMeta.
* refactor(channels): split chat-meta accessors along the SDK contract
getChatChannelMeta keeps its shipped plugin-SDK signature (defined for
bundled ids, fail-loud on impossible misses); new findChatChannelMeta
carries the drift-tolerant optional contract for core auto-enable and
formatting paths.
* fix(qa-channel): own channel metadata instead of a guaranteed-undefined catalog lookup
qa-channel spread getChatChannelMeta over an id that is never in the
bundled catalog, shipping an empty setup meta by accident; the fail-loud
SDK accessor exposed it. The channel now declares its metadata once.
* fix(gateway): heartbeat projection lookahead is optional at the transcript tail
expectDefined wrapped messages[i + 1] whose absence on the final message
is the normal case; the adjacent ternary already handled it. Restores
the plain optional read with an explicit guard in the pair condition.
* fix(plugin-sdk): channel plugin factory tolerates non-bundled channel ids again
createChannelPluginBase spreads bundled catalog meta for ANY channel id,
where absence is the normal case for external plugins; the resolver is
honestly optional again while the exported bundled-id accessor keeps the
fail-loud contract.
* fix(core): spreads of optional config sections stay optional
Fresh-setup and first-install paths (crestodian setup inference, hook
installs, agent config base, target agent models) legitimately lack the
section being rebuilt; spreading undefined is the shipped {} semantics.
Removes the remaining gratuitous assertion wraps found by tree audit.
* fix(launchd): verify profile updater jobs by metadata
* fix(launchd): bind updater proof to job metadata
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): show last error when status probe fails
* fix(gateway): scope status log diagnostics
---------
Co-authored-by: wAngByg <wAngByg@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>