The compaction safeguard treated a successful keyless auth resolution
(ok: true with neither apiKey nor headers) as missing credentials and
cancelled compaction. Bedrock providers using auth: "aws-sdk" sign
requests with SigV4 at send time and legitimately return no static key
or header, so every message on a session large enough to need compaction
failed with a false "could not resolve request credentials" error.
Trust the registry's ok:true success signal; it already returns ok:false
when auth genuinely cannot resolve. Add regression coverage for the
keyless SDK-managed auth path.
Co-authored-by: Roy Osherove <575051+royosherove@users.noreply.github.com>
* fix(signal): clean up signal-cli download temp dir on every exit path
installSignalCliFromRelease created a temp dir with fs.mkdtemp for the
download archive but never removed it. Every return path — including the
success return and the extraction-failure / binary-not-found errors — leaked
the directory plus the downloaded archive (signal-cli Linux-native archives
are ~50-130 MB). Repeated installs or retries accumulated multi-hundred-MB
temp dirs under the preferred tmp dir.
Wrap the body after mkdtemp in try/finally that recursively removes the temp
dir. The extracted binary is installed under CONFIG_DIR and is unaffected.
Co-Authored-By: Claude <noreply@anthropic.com>
* style(signal): drop unnecessary String() conversion in test spy
dir is already a string (mkdtemp return); String(dir) triggers the
no-unnecessary-type-conversion lint rule.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(signal): cover success-path temp dir cleanup with real archive
Add a success-path test that mocks the network fetch to return a real tar.gz
archive (containing a signal-cli binary) so installSignalCliFromRelease runs the
full download -> extract -> find-binary -> chmod path against real bytes, then
asserts the temp dir and downloaded archive are removed by the finally clause.
This complements the existing error-path test so both exit paths are covered.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(signal): use owned temp download workspace
* fix(signal): allow bounded native extraction
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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>