* fix(outbound): report delivery status in best-effort message send results
Best-effort sends (forced on for implicit message_tool_only source
replies) previously collapsed failed and partial_failed durable send
results into a success-shaped MessageSendResult, so agents saw a
normal-looking envelope while delivery had actually failed. Surface
deliveryStatus, a formatted error, sentBeforeError, and per-payload
outcomes without changing throw semantics.
* fix(outbound): centralize message delivery outcomes
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
* docs(changelog): split aggregate entries from code landing
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
[pytest>=8, <9] is parsed as two items; pip fails on '<9'.
Use ["pytest>=8,<9"] so pip gets one valid PEP 440 spec.
Pre-commit-only fix from closed#39109.
* fix(whatsapp): wrap JSON.parse with try-catch in auth store and test helpers
Add defensive try-catch around JSON.parse calls in WhatsApp extension
to prevent crashes from corrupted state files.
- restoreCredsFromBackupIfNeeded: wrap creds.json/backup validation
JSON.parse with try-catch; corrupted creds.json now properly falls
through to backup restoration instead of skipping it entirely
- updateLastRouteMock: wrap JSON.parse with try-catch, initialize
empty store on corrupted file
* test(whatsapp): add regression test for malformed creds.json longer than one byte
- Add a focused regression test for the exact case ClawSweeper
flagged: readWebCredsJsonRawSync returns non-null content for
files with stat.size > 1, so malformed JSON like "{x" (2 bytes)
reaches JSON.parse — the inner try-catch now catches the parse
failure and falls through to backup restoration
- Without this patch, JSON.parse("{x") throws to the outer catch
and restoreCredsFromBackupIfNeeded returns false, skipping backup
🦞 diamond lobster: L2 evidence (real function call + real filesystem objects)
Ref. https://github.com/openclaw/openclaw/pull/99070
* fix(whatsapp): restore malformed creds from backup
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
* docs(changelog): defer credential recovery entry to aggregate
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
* fix(whatsapp): cache bot's own outbound messages for quote metadata
When a user swipe-replies to a message the bot itself sent, the outbound
quote-key lookup misses the inbound-only metadata cache and falls back to
fromMe:false with the replying user's JID as participant. That mismatched
quoted.key is silently dropped by WhatsApp Desktop, so the bot's reply
bubble never renders there (it renders on Android, which is more lenient).
Cache quote metadata for the bot's own outbound messages at the send
choke point (rememberOutboundMessage) with fromMe:true and the bot's own
participant JID (group only; omitted for direct chats, matching WhatsApp
semantics). Future swipe-reply lookups then build a correct quoted.key.
Closes#91445.
* fix(whatsapp): preserve outbound quote metadata
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
* docs(changelog): defer quoted replies entry to aggregate
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Restore the maintainer-requested collapsed <details> failure callout that
a parallel session reverted, adapt the e2e expectations, and drop the
PR-side CHANGELOG edit (changelog is release-generation-owned).
* fix(gateway): prevent restart loops after terminal WhatsApp disconnects
Track `terminalDisconnect` through the WhatsApp status controller, channel
runtime snapshot, and `ChannelAccountSnapshot` so the health-monitor and
the `ChannelManager` task-exit handler both skip auto-restart when Baileys
signals a terminal session end (loggedOut / connectionReplaced).
Adds a `terminal-disconnect` `ChannelHealthEvaluationReason` so the policy
layer returns a stable, named reason rather than falling through to
`not-running`, preventing unbounded WebSocket/heap growth on multi-tenant
gateways. Fixes#78419.
* fix(gateway): prioritize terminal disconnect recovery
Co-authored-by: openperf <16864032@qq.com>
* docs(changelog): move WhatsApp restart fix to unreleased
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
694f5ac7e5 added a temp-dir import to test/scripts/package-git-fixture.test.ts
without updating the top-level helper routing expectation, breaking the
checks-node-compact-small-whole-2 shard on main and PR merge runs (same
class as 824d8af0d6).
* feat(scripts): serialize pr prepare gates and add remote testbox test gate
Concurrent scripts/pr gate runs across .worktrees queued on the shared
heavy-check lock mid-test: the queued run's children hit the 10-minute
lock timeout while its unlocked build stage piled CPU load onto the
holder's vitest shards, which then stalled past the 120s no-output
watchdog and were SIGTERMed with zero real test failures (observed
landing PRs #99935/#100026 on a loaded maintainer Mac).
- scripts/pr-gates-lock.mjs holds the shared heavy-check lock for the
whole local gate block; gate stages inherit the existing *_LOCK_HELD
child contract, so concurrent gate runs now queue as whole units
before their first command.
- OPENCLAW_PR_GATES_REMOTE=testbox runs the full-suite pnpm test gate on
a Blacksmith Testbox via scripts/crabbox-wrapper.mjs (same delegation
as check:changed). The tbx_ lease id and Actions run URL from the
crabbox --timing-json report are recorded in .local/gates.env
(REMOTE_GATES_*) and .local/prep.md. Local remains the default;
pnpm build/check stay local.
Formatting verified with the primary checkout's oxfmt (hook bypassed:
linked worktree has no hydrated node_modules).
* fix(scripts): refresh the gate stamp when lease-retry gates rerun for a rebased head
The lease-retry path reran build/check/test for the rebased prep head but
left .local/gates.env describing the pre-push head, so prep.md/prep.env
attributed stale evidence (including the new remote testbox lease id) to
the pushed commit. Extract write_gates_env_stamp as the single stamp
writer, rewrite the stamp from the retry path for all modes, and re-source
gates.env in prepare_push after the push settles.
Found by autoreview (codex/gpt-5.5); formatting verified with the primary
checkout's oxfmt (hook bypassed: linked worktree has no node_modules).
* fix(scripts): harden remote PR gate evidence
* fix(scripts): serialize complete gate setup
* fix(scripts): clear stale docs gate proof
* fix(imessage): only warn about empty group allowlist when messages actually drop
With groupPolicy="allowlist", a non-empty effective groupAllowFrom admits
group messages even when channels.imessage.groups is empty (senderFilterBypass
in src/config/group-policy.ts), so the startup warning "Every inbound group
message will be dropped" fired as a false positive for that configuration.
The warning now mirrors the runtime gate's effective sender allowlist (same
allowFrom fallback + legacy chat-target merge) and fires only when both the
groups registry and the effective group sender allowlist are empty - the only
startup-provable drop-all config. The message now names groupAllowFrom as the
fix, since adding groups entries alone leaves the sender gate blocking.
Docs: describe the two group gates' warnings separately with per-warning
remedies instead of implying both fire for the same config.
* docs(imessage): format migration guide
* fix(imessage): warn on empty group sender allowlist
* docs: move iMessage fix to unreleased
* fix(qqbot): publish disconnected channel status when the gateway closes or gives up
The QQBot channel only ever set connected: true (onReady/onResumed);
no close path updated the status, so a fatal close (bot banned or
offline, 4914/4915) or reconnect exhaustion left channels.status
claiming a live connection forever, and the channel-health monitor
never saw connected=false for a dead gateway.
Thread an onDisconnected callback from GatewayConnection.handleClose
(fatal and pre-reconnect branches) and the reconnect-exhaustion path
through the engine/bridge layers into channel.ts, which now records
connected: false and, for fatal closes, the close reason as lastError.
The running flag stays owned by the gateway lifecycle store, matching
the sibling channel convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): import ChannelAccountSnapshot from the channel-contract subpath
The monolithic openclaw/plugin-sdk root entry is a legacy surface;
plugin-sdk contract guardrails and extension boundary checks require
focused subpath imports in bundled plugin sources.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): ignore stale socket closes from superseded gateway connections
A server-driven RECONNECT / INVALID_SESSION tears the old socket down
and brings up a replacement; the old socket's close event can arrive
after the replacement is live. Reacting to it again would tear down the
new socket and regress the connected status, so the close handler now
ignores closes from sockets that are no longer current.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): harden disconnect status recovery
* fix(qqbot): report opcode-driven reconnects
* fix(qqbot): keep fatal disconnect health visible
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
17a1993f13 made test/scripts/e2e-shell-tempfiles.test.ts import the
temp-dir helper without updating the helper-routing expectation, so
checks-node-compact-small-whole-2 fails on every PR merge snapshot.
The plugin-approvals schema gained approvalReviewerDeviceIds (#100251)
without refreshing the generated Swift model, so the bundled-protocol
drift gate fails on every PR. Mechanical pnpm protocol:gen:swift output.