* fix(apple): prevent stale model thinking state
* test(apple): complete attachment transport fixture
* fix(apple): scope model patch ordering by route identity
* chore(apple): sync native CI metadata
* fix: normalizeMediaSource should handle uppercase FILE:// URI schemes
URI schemes are case-insensitive per RFC 3986. normalizeMediaSource
only matched lowercase 'file://', so MEDIA:FILE:///tmp/generated.png
was left in assistant text instead of being extracted as a local
media attachment.
Replaced startsWith + replace with a case-insensitive regex to handle
FILE://, file://, and any mixed-case variant.
Fixes#103473
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(media): handle spaced uppercase file URLs
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): keep thread-binding persona UTF-16 safe
resolveThreadBindingPersona truncated the persona label with a raw .slice(0, THREAD_BINDING_PERSONA_MAX_CHARS). When an emoji or other astral code point straddles the 80-unit limit, the raw slice keeps a dangling high surrogate, so the Discord thread-binding persona (webhook display name) can carry a lone surrogate. Route the clamp through the shared truncateUtf16Safe primitive so the boundary code point is dropped whole. Adds a regression test.
* test(discord): assert exact persona boundary
* docs(changelog): credit Discord persona fix
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(gateway): session worktree targeting, branch listing, and group catalog
- sessions.create accepts worktreeBaseRef/worktreeName (write scope) and
execNode (admin); worktree binding persists on the session entry as
worktree { id, branch, repoRoot } and projects onto session rows with
execNode so UIs can show checkout/branch/node state
- new worktrees.branches RPC lists local/remote branches (no fetch) for
base-ref pickers; worktrees.remove now reports snapshotError
- sessions.delete reports preserved dirty checkouts as worktreePreserved
- gateway-owned session group catalog in the shared state DB with
sessions.groups.list/put/rename/delete; rename/delete update member
categories server-side without bumping recency; sessions.patch absorbs
ad-hoc categories into the catalog
- group session display names prefer the human chat title (subject or
space #channel) over stored compact tokens
Part of #103431
* fix(gateway): route group category updates through the session accessor and regen protocol models
- bulk member-category rename/clear uses applySessionEntryReplacements
instead of legacy updateSessionStore call sites (session accessor
boundary guard)
- regenerate Swift GatewayModels for the worktrees.branches schemas,
snapshotError, and new sessions.create params
Part of #103431
* fix(gateway): resolvable remote branch refs and workspace-scoped branch listing
- worktrees.branches returns remote-only branches remote-qualified
(origin/feature-a) so every advertised name works as a worktree base ref
- write-scoped worktrees.branches callers are limited to configured agent
workspaces; other host paths require operator.admin, matching the
sessions.create cwd bar
Part of #103431
* fix(gateway): guard worktree name reuse by owner and env-scope group transactions
- managedWorktrees.create rejects a caller-chosen name whose live or
restorable record belongs to a different owner, so write-scoped
sessions.create cannot bind a session into another session's or a
manual checkout
- session group catalog writes run their SQLite transaction on the same
env-scoped handle as their statements, keeping OPENCLAW_STATE_DIR
overrides atomic and away from the default state DB
Part of #103431
* fix(gateway): use truncateUtf16Safe for voice-wake trigger truncation
Replace naive .slice(0, 64) with truncateUtf16Safe() in
normalizeVoiceWakeTriggers to prevent surrogate pair splitting
in user-configured voice wake trigger phrases.
Voice wake triggers are user-configurable text strings that may
contain emoji or non-BMP characters. A naive .slice(0, 64) at
a surrogate pair boundary produces a lone surrogate, which
corrupts the trigger text.
Co-Authored-By: Claude <noreply@anthropic.com>
* test: add proof scripts for C1 sanitize and voice-wake UTF-16
* chore: remove unrelated proof script from voice-wake PR
ClawSweeper review: the console-sanitization proof script belongs
to #103226, not this gateway voice-wake fix. Remove it to keep the
branch clean.
Ref: #103210 review
* fix(gateway): harden voice wake unicode boundary
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: 429 errors without rate-limit wording skip the same-model retry
Aggregated providers such as OpenRouter provider pools surface transient
upstream throttles as a bare "429 <generic text>" with no RPM/TPM wording
and no Retry-After header. resolveShortWindowRateLimitRetry required one
of those hints, so these errors bypassed the same-model retry ladder and
failed the turn on the first attempt.
Treat a status-prefixed 429 as short-window once the long-window guard
(quota, usage, billing wording) has filtered its phrasings, and let the
existing backoff ladder pace the retry.
* test(agents): streamline bare 429 coverage
---------
Co-authored-by: Altay <altay@hey.com>