* fix(gateway): guard thinkingLevel re-validation against only relevant patch fields
The second thinkingLevel validation block used `if (next.thinkingLevel)`,
which enters on ANY patch when the session already has a thinkingLevel
inherited from the existing entry. This caused unnecessary model catalog
loading and could silently delete or modify the existing value.
Fix: change guard to also check that the patch explicitly touches
thinkingLevel or changes the model (which may alter the effective
provider/model that thinkingLevel is validated against).
Ref: BUG-002 (local finding)
Co-Authored-By: Claude <claude@anthropic.com>
* test(gateway): prove session patch catalog isolation
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(usage-bar): cap warnedTemplateOverrides warn-once dedupe cache
Replace unbounded warnedTemplateOverrides Set with createDedupeCache(maxSize=256)
for consistency with the bounded fileCache Map (MAX_CACHED_TEMPLATE_FILES=64)
already present in the same file.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(usage-bar): bound invalid-template warnings
Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(infra): cap session-maintenance-warning dedupe cache with LRU eviction
The warnedContexts Map accumulated every warned session key forever
with no eviction, TTL, or size cap. A long-running gateway would grow
this unboundedly.
Add a 4 096-entry LRU cache with touch-on-read so frequently re-warned
sessions survive and old entries are evicted on overflow.
* fix(test): restore original unicode escapes and add LRU eviction tests
* fix(test): seed eviction entries with real warning context keys
ClawSweeper P3: the eviction test seeded but the production
buildWarningContext computes . The mismatch
meant params1 could redeliver because the context changed, not because
LRU eviction actually worked. Seed the exact context pattern so the
test fails when eviction breaks.
* fix(infra): bound maintenance warning cache
Co-authored-by: sunlit-deng <sunlit-deng@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: sunlit-deng <sunlit-deng@users.noreply.github.com>
* fix(anthropic): don't crash models list when a configured Sonnet 5 model has no cost
applyAnthropicSonnet5Cost read params.model.cost.input unconditionally, so
'openclaw models list' aborted with 'Cannot read properties of undefined
(reading input)' whenever config supplied an anthropic Sonnet 5 model row
without cost metadata (e.g. an agents.defaults.models runtime binding).
Guard with optional chaining — a costless model now falls through and gets
the canonical Sonnet 5 cost applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(models): normalize missing Sonnet 5 costs
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* chore(plugin-sdk): refresh API baseline
* ci(plugin-sdk): update surface budgets
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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>