* [AI] fix(node-cli): warn when systemd user lingering is disabled after install
openclaw node install now detects when systemd user lingering is off and
warns the operator (text + JSON) to run 'sudo loginctl enable-linger <user>'.
Without lingering, the user-level node service is torn down when the last SSH
session ends, so the node silently goes offline after logout.
The check is read-only and never auto-enables lingering, matching the
operator-consent policy used elsewhere. It runs only on the verified-success
path: an optional onVerified hook is added to installDaemonServiceAndEmit
that fires after service.isLoaded() confirms the service is loaded and before
the success payload is emitted. The linger diagnostic runs there, so a failed
install or verification failure never carries a linger warning (avoids
misdirecting the operator to fix lingering for a service that was not
successfully installed). The already-installed short-circuit warns separately.
Skipped on non-Linux and when systemd user service is unavailable.
Adds unit tests for both paths, the linger=yes no-op, the install-failure
isolation, the verification-failure no-warn regression, and the
systemd-unavailable skip, plus response.test.ts cases covering onVerified
running on success and failing safely when it throws. The
readSystemdUserLingerStatus mock is typed with the full linger union to
satisfy tsgo. Documents the linger step in docs/cli/node.md and
docs/nodes/troubleshooting.md.
Real-behavior evidence captured on a Linux host by toggling
loginctl disable-linger/enable-linger and running the real install flow:
linger=no emits the warning on successful install (text + JSON) and on the
already-installed path; linger=yes emits nothing; a failed install or
verification failure emits no warning.
Fixes#107033
Co-Authored-By: deepseek-v4-flash <noreply@anthropic.com>
* fix(node-cli): align linger user with service owner
* docs(node): narrow crash-loop claim to gateway units
The duplicate-scope guard that raises on two managers running the same unit
name is enforced for gateway units (two supervisors on the same port SIGTERM
each other in a restart loop); assertNoSystemGatewayOwnership returns early
for node services, so claiming node services crash-loop misattributes gateway
behavior. Qualify the troubleshooting note accordingly.
Addresses ClawSweeper P3 finding on PR #118430.
* fix(systemd): align linger checks with service owner
* test(doctor): align linger status mock contract
* style(doctor): format linger mock
* test(wizard): mock systemd service account
---------
Co-authored-by: deepseek-v4-flash <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(google): accept base64url in Live audio
* fix(google): normalize base64url in one pass
* fix(google): normalize base64url at every Google media boundary
* fix(google): normalize image media and avoid double music validation
* test(google): exercise URL-safe base64 fixtures
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* [AI] fix(plugins): warn when registerHook uses a typed hook event name
api.registerHook registers into the legacy internal-hook path, while typed
lifecycle events (before_tool_call, message_received, ...) are dispatched
exclusively by the typed hook runner. Registrations under typed names
silently never fired since #117372 moved the write to legacyInternalHooks.
Emit a registration diagnostic pointing to the public api.on(...) API so
plugin authors see the no-op at load time instead of trusting a false
"loaded".
Related to #116965
Co-Authored-By: deepseek-v4-flash <noreply@anthropic.com>
* [AI] fix(plugins): point registerHook comment at public api.on API
Condense the inline comment in createToolHookRegistrars and replace the
internal registerTypedHook migration reference with the public api.on(...)
API, matching the emitted warning and hooks docs. Behavior and tests
unchanged.
---------
Co-authored-by: deepseek-v4-flash <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(gateway): make doctor dreaming timestamp comparators NaN-safe
compareDreamingEntryByRecency and compareDreamingEntryByPromotion used
`Number.isFinite(aMs) || Number.isFinite(bMs)` then `return bMs - aMs`.
When exactly one side parsed to NaN (non-empty malformed timestamp), the
guard still entered the branch and returned NaN - bMs, producing NaN and
leaving Array.sort order undefined for the doctor.memory.status dreaming
entry list. Coerce unparseable timestamps to -Infinity via a shared
parseDreamingTimestampMs helper and return a finite -1/0/1 from the
timestamp branch so the sort stays deterministic.
* fix(memory-core): incorporate producer-side NaN-safe timestamp comparator
ClawSweeper [P1] finding: gateway-only fix is incomplete because Memory Core
has the same mixed-NaN comparator BEFORE the gateway merges lists. A valid
entry discarded upstream cannot be recovered at the gateway boundary.
Incorporate the producer-side fix:
- Export compareStoreTimestampDesc from short-term-promotion-utils.ts
- Replace the buggy raw Date.parse + bMs-aMs pattern in
short-term-promotion-stats.ts with the NaN-safe compareStoreTimestampDesc
This makes #118749 self-contained: both gateway doctor.ts AND memory-core
stats.ts comparators are NaN-safe. The companion #118750 now only needs to
cover the recency ranking fix (ageDays/recency calculation).
* fix(memory): preserve valid dreaming diagnostics
* style(memory): format dreaming imports
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
The stale plugin config repair report counted heartbeat and
modelByChannel hits per surface but listed all stale channel ids in
parentheses, so a heartbeat entry could claim channel ids that were
never heartbeat targets. List the deduplicated ids of the hits for
each surface instead, matching the other change entries.