- Use base-path-relative manifest URL (no leading slash) so PWA works
under non-root base paths
- Classify push.web.* methods in WRITE_SCOPE so they work for operator
sessions without admin scope
- Refresh webPushPermission in the finally block of handleWebPushSubscribe
so denied permission is immediately reflected in the UI
- Check OPENCLAW_VAPID_PUBLIC_KEY/PRIVATE_KEY env vars before falling
back to persisted keys, so operators can share a stable VAPID identity
across multiple gateway instances
- Wrap VAPID key bootstrap in async lock to prevent concurrent first-run
callers from generating different keypairs
- Add test for env var precedence
Make the Control UI installable as a Progressive Web App and add Web Push
notification infrastructure so the gateway can send browser notifications
to subscribed clients.
PWA shell:
- Add manifest.webmanifest with app name, icons, standalone display
- Add service worker with cache-first for hashed assets, network-first
for HTML/API, push event handler, and notification click focus
- Register service worker from main.ts
- Serve .webmanifest with correct content type from gateway
- Add worker-src 'self' to CSP
Web Push server (src/infra/push-web.ts):
- VAPID key auto-generation and persistence to state dir
- Subscription CRUD with async-locked atomic JSON storage
- Send and broadcast via web-push library
- Mirrors push-apns.ts patterns
Gateway RPC (4 new methods):
- push.web.vapidPublicKey: returns VAPID public key for PushManager
- push.web.subscribe: registers browser push subscription
- push.web.unsubscribe: removes subscription by endpoint
- push.web.test: broadcasts test notification to all subscribers
Client-side push manager (ui/src/ui/push-subscription.ts):
- Permission request, VAPID key fetch, PushManager subscribe/unsubscribe
- Gateway RPC wiring for registration and test
Settings UI:
- New "Notifications" virtual section in Communications tab
- Shows browser support, permission state, subscription status
- Subscribe/unsubscribe/test buttons
Includes 10 unit tests for push-web subscription CRUD and VAPID keys.
Summary:
- Show full date and time in Control UI chat message footers.
- Collapse assistant model/token/context metadata behind an explicit Context disclosure.
- Update changelog attribution guidance to allow multi-author credited entries.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub CI passed on f071a38177
* fix(heartbeat): clamp scheduler delay to Node setTimeout cap (#71414)
When `agents.defaults.heartbeat.every` resolves to >2_147_483_647 ms
(~24.85d), the previous scheduleNext() called setTimeout with the raw
delay. Node clamps any delay > 2^31-1 to 1 ms, fires the callback, and
the heartbeat re-arms with the same oversized value - a tight loop that
floods the log with TimeoutOverflowWarning and crashes the gateway with
exit code 1.
Clamp the computed delay to HEARTBEAT_MAX_TIMEOUT_MS (2_147_483_647)
before calling setTimeout. The worst case is now one heartbeat every
~24.85d instead of crash-loop. Warn once per process when clamping
fires, so a misconfigured "365d" remains visible without flooding.
This is a defense-in-depth fix at the scheduler layer; loadConfig-level
rejection is a broader change with more blast radius and a separate
question (some users may legitimately want "every: 365d" to mean
"effectively never"). The clamped behaviour is closer to that intent
than the crash is.
Test: new scheduler test sets heartbeat.every="365d" with fake timers,
advances 60s, and asserts runSpy was never called (with the bug, it
would be called ~60_000 times).
* style: format heartbeat scheduler clamp
* fix: share safe timeout delay clamp (#71478) (thanks @hclsys)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Add bounded outbound message delivery lifecycle diagnostics and OTEL export without message body, recipient, room, media path, or raw channel result data.
Polish the Control UI markdown preview chrome and sidebar raw-text behavior.
- Add the upgraded preview dialog/sidebar chrome and tighten related CSS coverage.
- Show workspace-relative paths in the markdown preview dialog instead of absolute filesystem paths.
- Preserve raw markdown source for idempotent raw-text toggles.
- Align browser plugin-sdk facade export parity for DEFAULT_BROWSER_ACTION_TIMEOUT_MS.
- Stabilize the gateway update channel test by waiting for the async update runner call.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/views/agents.test.ts ui/src/ui/views/chat.test.ts src/plugins/contracts/plugin-sdk-subpaths.test.ts src/gateway/server.roles-allowlist-update.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub checks green on ebbe96fc88