Codex review on c905b3a flagged two real issues:
1. Explicit `service: 'sms'` (e.g. `sms:+15551234567`) was still being
routed to iMessage whenever both chats existed, because the iMessage-
first preference was hardcoded. Make the preferred service
parameterized off `target.service`: SMS for explicit `sms` intent,
iMessage otherwise (covers `imessage`, `auto`, and undefined).
2. Codex also flagged on the prior commit that breaking on
`participantIMessageMatch` could skip a higher-priority direct
`iMessage;-;<handle>` match on a later page. Direct > participant in
our preference order, so the early break was unsound. Remove it
entirely \u2014 only a direct preferred-service match can short-circuit
pagination (and that branch already `return`s immediately).
Final return preference is now:
participantPreferredMatch
\u2192 directHandleOtherServiceMatch
\u2192 participantOtherServiceMatch
\u2192 directHandleUnknownServiceMatch
\u2192 participantUnknownServiceMatch
Adds three regression tests:
- explicit `service: 'sms'` prefers SMS direct over iMessage direct
- explicit `service: 'sms'` falls back to iMessage when no SMS exists
- later-page direct iMessage beats earlier participant iMessage
Greptile flagged that directHandleIMessageMatch was misnamed: real
iMessage;-;<handle> direct hits return immediately, so the variable
only ever held unknown-service GUIDs. It was then used in the early-
break guard, which could stop pagination before a genuine iMessage
participant match on a later page was discovered.
- Rename to directHandleUnknownServiceMatch to reflect actual usage
- Drop it from the early-break guard (only break on real iMessage hit)
- Reorder return preference: participant iMessage > SMS direct >
unknown-service direct > SMS participant
- Add regression test covering the unknown-service short-circuit case
When sending to a handle (phone number) that has both an iMessage and an
SMS chat in BlueBubbles, resolveChatGuidForTarget returned whichever chat
it encountered first in the page. This caused messages to silently
downgrade from iMessage to SMS for recipients who use iMessage, which is
never what the user wants.
The fix tracks iMessage and SMS matches separately and returns the
iMessage match when available, falling back to SMS only when no iMessage
chat exists for the handle. Applies to both direct handle matches (chat
guid contains the handle) and participant matches.
Also short-circuits page iteration once an iMessage match is found.
Tests: 5 new unit tests covering iMessage preference, SMS fallback, and
participant-match preference.
* bluebubbles: fall back unsupported reactions to love
iMessage tapback only supports love/like/dislike/laugh/emphasize/question.
Previously, `normalizeBlueBubblesReactionInput` threw when the input did
not map to one of those (e.g. a non-standard unicode emoji like 👀 used
to mean "seen, working on it"), which aborted the whole reaction request
and left the user with no feedback.
This splits the normalizer into a strict and lenient variant:
- `normalizeBlueBubblesReactionInputStrict` throws on unsupported input
and is used by validator-style callers (e.g. `resolveBlueBubblesAckReaction`
in monitor-processing.ts) that rely on the throw to detect misconfigured
ack reactions and skip them cleanly. This preserves the previous silent-skip
+ warn-once behavior for ack reactions configured with an unsupported
emoji.
- `normalizeBlueBubblesReactionInput` stays lenient and falls back to
`love` (or `-love` when removing) on unsupported input, so agent-driven
`sendBlueBubblesReaction` still produces a visible tapback instead of
failing the whole reaction request. Contract errors (empty input)
continue to bubble up.
`love` is chosen over `like` as the neutral default: `❤️` reads as a
general acknowledgment across chat norms, while `👍` carries an
agreement connotation that does not match the "seen, working on it"
semantic.
* CHANGELOG: note BlueBubbles reaction fallback
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* test(agents): expect timing fields in killed-run outcome
Aligns the steer-restart killed-run test with the timing fields added to
subagent run outcomes in #68726. The production code now returns
startedAt/endedAt/elapsedMs alongside status and error on the error
outcome, but this test's toEqual still asserted only status+error, so it
has been failing on main since #68726 landed. Uses the same expect.any(Number)
matcher already in use a few lines below for the ended hook payload.
* test(gateway): register ops agent in sessions.create task-start test
The "sessions.create can start the first agent turn from an initial task"
test triggers the auto chat.send path by passing `task:`. After #65986
added a deleted-agent guard to chat.send, an unregistered `ops` agent
triggers the reject path and the auto-started run never happens, so
runStarted comes back false.
Register `ops` via testState.agentsConfig (matching the pattern already
used by other ops-agent tests in this file) so the guard lets chat.send
through and the first turn starts as expected.
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* qa-lab: harden CI defaults and failure semantics for live lanes
* qa-lab: add unit tests for suite progress logging defaults
* qa-lab: cover malformed multipass summary edge cases
* qa-lab: share suite summary failure counting helper
* qa-lab: test allow-failures parse wiring and sanitize progress ids
* fix: note qa CI live-lane defaults in changelog (#69122) (thanks @joshavant)
Add a Matrix QA scenario that removes an observer from the running account group allowlist and verifies the existing gateway stops replying without relying on a channel restart.
The scenario disables generic config reload and defers restart during the probe so it specifically covers the Matrix handler per-message live allowlist read.
Add a qa-matrix contract scenario that sends a Matrix self MXID-prefixed
control command from an observer and expects no SUT reply. This captures the
regression fixed by the Matrix command precheck change.
* WhatsApp: harden auth persistence and backup recovery
* WhatsApp: model unstable auth state across runtime and setup
* WhatsApp: recover login and monitor startup from unstable auth
* Channels: surface auth stabilizing in status and health
* Gateway protocol: add channels.start surface
* Gateway: reconcile local channel runtime after CLI login
* Channels UI: reflect recovered login start state
* Changelog: note WhatsApp auth stabilization
* Gateway: fix lint in call test
* fix(browser): discover CDP websocket from bare ws:// URL before attach
When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as:
Browser attachOnly is enabled and profile "openclaw" is not running.
even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (#68027).
Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip.
Fixes#68027
* test(browser): add seeded fuzz coverage for CDP URL helpers
Adds property-based / seeded-fuzz tests for the URL helpers the
attachOnly CDP fix depends on (#68027):
- isWebSocketUrl
- isDirectCdpWebSocketEndpoint
- normalizeCdpHttpBaseForJsonEndpoints
- parseBrowserHttpUrl
- redactCdpUrl
- appendCdpPath
- getHeadersWithAuth
Follows the existing repo convention (see
src/gateway/http-common.fuzz.test.ts): no fast-check dep, small
mulberry32 PRNG + hand-rolled generators, deterministic per-describe
seeds so failures are reproducible.
Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements,
67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered
lines are inside the WS sender internals (createCdpSender,
withCdpSocket, fetchCdpChecked rate-limit branch), which require
integration-style mocks and are unrelated to the attachOnly fix.
* test(browser): drive cdp.helpers/cdp/chrome to 100% coverage
Lifts the three files touched by the #68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications.
* fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs
When /json/version discovery is unavailable (or returns no
webSocketDebuggerUrl), fall back to treating the original bare ws/wss
URL as a direct WebSocket endpoint. This preserves the #68027 fix for
Chrome's debug port while restoring compatibility with Browserless/
Browserbase-style providers that expose a direct WebSocket root without
a /json/version endpoint.
Priority order for bare ws/wss cdpUrl inputs:
1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged)
2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a
webSocketDebuggerUrl use it; otherwise fall back to the original URL
as a direct WS endpoint
3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged)
Affected call sites: isChromeReachable, getChromeWebSocketUrl,
createTargetViaCdp.
Also renames a misleading test ('still enforces SSRF policy for direct
WebSocket URLs') to accurately describe what it tests: SSRF enforcement
on the navigation target URL, not on the CDP endpoint.
New tests added for all three fallback paths. Coverage remains 100% on
all three touched files (238 tests).
* fix: browser attachOnly bare ws CDP follow-ups (#68715) (thanks @visionik)