* fix(agent-runner-memory): use truncateUtf16Safe for memory flush error truncation
Replace raw UTF-16 slices in buildMemoryFlushErrorPayload and
truncateMemoryFlushErrorMessage with truncateUtf16Safe to prevent
surrogate pair splitting in error messages shown to users.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* test(auto-reply): cover UTF-16-safe memory errors
* test(auto-reply): tighten UTF-16 memory error coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(heartbeat): remove static iteration cap in seekNextActivePhaseDueMs
* fix(heartbeat): floor seek step at 60s to bound active-hours seek iterations
The static MAX_SEEK_ITERATIONS cap (10,080 ≈ 7 days / 1 min) was removed
in the previous fix but replaced the risk with unbounded iteration for
sub-minute intervals. Floor the seek step at 60s via MIN_SEEK_STEP_MS
so the loop is always bounded by MAX_SEEK_HORIZON_MS / 60_000 = ~10,080
iterations regardless of the configured intervalMs. Active-hours windows
are minute-granular, so finer steps add no precision.
* fix(heartbeat): batch seek in whole-interval multiples to preserve phase alignment
Use the smallest intervalMs multiple >= 60s as the seek step so every
candidate checked is reachable from startMs by whole intervalMs steps.
This fixes the phase-alignment regression from the previous fix where
Math.max(intervalMs, 60_000) could return off-phase slots for non-divisor
sub-minute intervals like 45s.
Added a regression test for 45s interval to verify the returned candidate
is phase-aligned.
* fix(heartbeat): check every phase candidate, bound only by 1M-iter safety cap
Drop batched seeking (intervalMs-multiple steps) because any multiplier > 1
can skip phase slots inside narrow active windows. Instead check every
phase candidate, with a 1M-iteration safety cap that prevents event-loop
stalls from sub-ms intervals while covering the full 7-day horizon for
intervals >= 605ms.
Added regression test for 59s interval / 1min active window where batched
steps (118s) would skip from 08:59:44 past 09:00:43 to 09:01:42.
* fix(heartbeat): bound sub-second seek with batch-step + backward-walk
Replace the 1M-iteration per-candidate cap with a two-tier strategy:
- intervalMs >= 1s: per-candidate scan (naturally bounded by horizon)
- intervalMs < 1s: batch in whole-interval multiples >= 1s, with
backward walk on inactive->active transitions to find the earliest
phase-aligned active slot
Max iterations <= 604,800 in all cases (~10 ms for trivial predicates,
~0.6-6 s for production isWithinActiveHours with Intl.DateTimeFormat).
Added tests:
- Sub-second interval, startMs already active -> returns directly
- Sub-second interval, inactive->active transition -> backward walk
finds earliest phase-aligned slot
* fix(heartbeat): unify seek at >= 30s batch step, max 20,160 predicate calls
Use a single batched-seek path for all intervals: step in whole-interval
multiples >= 30 s with backward walk on inactive->active transitions.
For intervalMs >= 30 s (30 s, 59 s, 60 s+): multiplier = 1, effectively
per-candidate with no batch overhead.
For intervalMs < 30 s (1 s, 500 ms, 1 ms): batched at >= 30 s, backward
walk finds the first active phase-aligned slot in the window.
Max predicate calls <= 20,160 for any interval (30 s minimum step /
7-day horizon). Production isWithinActiveHours (Intl.DateTimeFormat)
cost: ~20–200 ms worst case vs 0.6–6 s for the per-candidate approach.
* fix(heartbeat): bound active-hours seek cost
* fix(heartbeat): resolve current main
* test(heartbeat): isolate active-window regression
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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(agents): isolated cron busts prompt prefix cache via per-run session id
Isolated cron runs carry a per-run :run:<id> session scope (#91685) rendered
verbatim into the cached system-prompt Runtime line, re-busting byte-exact
prefix caching for the tool catalog after it every run (#96677, #43148 class).
buildRuntimeLine now renders the stable base session key and drops the per-run
id the run scope duplicates; parseCronRunScopeSuffix is gated to the
isolated-cron key shape so a :run: segment in any other session key is never
truncated.
* fix(agents): preserve mixed-case cron run markers
* test(agents): cover rotated cron session identity
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(voice): universal talk waveform driven by real audio levels
One shared Siri-style talk animation across iOS, watchOS, macOS, and
Android (TalkWaveformView + exact Compose port), replacing per-platform
fakes: synthetic speaking pulses, constant speech-detected power, static
Android bar rows, flat realtime listening. Listening/recording follow
live mic levels on every route; agent speech follows the real playback
envelope (AVAudioPlayer metering + playback-aligned PCM envelope +
WebRTC stats); voice-note recording shows a live capture wave.
* fix(android): order waveform imports and sync native i18n inventory
* chore(i18n): resync native inventory after rebase
* fix(imessage): cap per-chat group-allowlist warn-once cache
Replace unbounded perChatWarned Set with createDedupeCache(maxSize=512)
to keep long-running iMessage monitor memory stable. The cache grows
with every distinct group chat the gateway sees; without a cap it can
accumulate entries indefinitely.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(imessage): prove warning cache eviction
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Add explicit Simplified Chinese glossary terms for the separate device- and node-pairing workflows and preserve the four CLI commands verbatim during translation.
Thanks @zeuming.
* feat(ui): drop the desktop top bar in favor of sidebar navigation
The Control UI top bar only rendered the OpenClaw › agent › page breadcrumb
on desktop, duplicating the sidebar. Desktop now renders no topbar row; the
row returns solely as the chat split toolbar's backdrop. Narrow viewports
keep the header (drawer toggle, brand, command-palette search) and now show
the brand across the whole drawer range. Routing already uses pushState, so
browser back/forward traverses navigation.
* feat(macos): add back/forward controls to the dashboard window
Native titlebar buttons next to the traffic lights drive WKWebView history,
which carries the Control UI's pushState navigation; enabled state tracks
canGoBack/canGoForward via KVO and trackpad swipe gestures are enabled too.
Back/forward traversals to entries from a replaced gateway endpoint cancel
silently instead of opening a dead URL in the system browser.
* fix(macos): keep the dashboard drag strip off the web content column
With the desktop topbar row removed, the 28px mid-window drag region sat on
top of the content column (chat thread has no top padding) and swallowed
clicks as window drags. Shrink it to a 12px edge strip; the sidebar region
over the reserved native-chrome padding stays the primary drag surface.