* fix(discord): guard JSON.parse against malformed API response bodies
Wrap JSON.parse(text) in requestDiscord with try/catch to prevent a malformed Discord API response body from throwing an unhandled SyntaxError and crashing the process. On parse failure, throw a DiscordApiError with a descriptive message so the caller can handle it gracefully.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* fix(clawsweeper): address review for automerge-openclaw-openclaw-97889 (validation-1)
* chore: trigger CI re-run for flaky checks-node-compact-small-whole-2
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* chore: trigger CI re-run for flaky runtime-inventory-drift-check
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* chore: trigger CI re-run for flaky checks-node-compact-large-whole-1
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* fix(discord): guard JSON.parse against malformed API response bodies
* fix(discord): guard JSON.parse against malformed API response bodies
---------
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
* fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257)
Fire-and-forget sessions_send (timeoutSeconds === 0) with announce
delivery runs the A2A ping-pong loop. For a cross-session send
(requester != target) the loop's first iteration feeds the target
agent's reply back into the requester session as a new turn. For a
normal requester that roundtrip is intended, but for an *isolated cron*
requester it injects reply context into the isolated run and causes an
agent feedback loop.
Narrow the fix to isolated-cron requesters only (detected by a session
key containing ":cron:" or channel "cron" -- the same signal used by
src/agents/subagent-registry.ts and set in
src/cron/isolated-agent/run.ts), NOT by timeoutSeconds. Gating on
timeoutSeconds was too broad: it disabled the intended ping-pong for
normal cross-session fire-and-forget sends.
Two hunks in src/agents/tools/sessions-send-tool.ts, both reusing one
`isIsolatedCronRequester` gate:
1. Force maxPingPongTurns to 0 in the runSessionsSendA2AFlow invocation
only for an isolated-cron requester. The a2a flow's ping-pong guard
(`maxPingPongTurns > 0`) then skips the requester-injection loop and
proceeds straight to the announce step in the TARGET session,
preserving fire-and-forget announce delivery. Normal requesters keep
the configured turn count.
2. Read baselineReply for fire-and-forget sends when same-session (prior
behavior) OR isolated-cron. Without a baseline fingerprint, a2a.ts
would treat pre-existing assistant text in the target session (e.g.
an unrelated concurrent cron's output) as "the reply" and
misattribute it. The normal cross-session fire leg's history-call
count is unchanged from origin/main. Read failures are tolerated so a
snapshot error never blocks accepting the send.
Tests (sessions.test.ts): an isolated-cron cross-session fire-and-forget
forwards maxPingPongTurns: 0; a normal-requester regression guard
(discord:group:req / channel discord) forwards the configured turn count
(not 0), proving the normal ping-pong is preserved. a2a flow tests
(sessions-send-tool.a2a.test.ts): with turns=0 + requester!=target the
requester is never stepped but the target is still announced, and a
baseline-matching reply is neither injected nor announced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): gate isolated-cron A2A on canonical cron-run classifier (#92257)
Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.
* refactor(agents): drop dead cron-channel A2A arm, gate on canonical key only (#92257)
The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.
Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.
* fix(agents): align cron A2A fallback baselines
* chore: prepare branch refresh
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
When keys is empty, keys[keys.length - 1] returns undefined, and the
previous ?? fallback would silently create a property with an empty
string key on the target object. Add an early return so empty keys are
a no-op instead of silently polluting the object.
* fix(gateway): distinguish reachable gateway from failed status probe
* fix(status): gate owns-port RPC recovery guidance on no stale gateway PIDs
inspectGatewayRestart can set health.healthy from bare reachability after
ownership attribution failed, while still returning a non-empty
staleGatewayPids. Printing owns-port recovery guidance in that case
contradicted the dedicated stale-PID diagnostic below it. Require
staleGatewayPids to be empty before treating healthy as owns-port proof.
* feat(cli): openclaw attach — launch Claude Code bound to a gateway session with scoped MCP tools
* fix(cli): use token-only MCP config for attach
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* Diagnose Windows LAN Gateway firewall blocks
* Fix Windows firewall diagnostic lint
* fix: gate gateway firewall diagnostics to local targets
* fix: keep firewall inspection off critical flows
Review on #98102: the durable-retry classifier treated every MediaFetchError
`fetch_failed` as retryable, but `fetch_failed` also covers permanent failures
(SSRF/guard denials, local Bot API path/read errors). On spooled replay those
would requeue forever with the user-facing warning suppressed.
Move the policy to its owner: add `isDurablyRetryableMediaFetchError` to
`src/media/fetch.ts`, defined as `shouldRetryMediaFetch` (the canonical in-loop
transient policy) plus shutdown/abort `fetch_failed`. A restart-window abort is
the primary inbound-media loss vector and is recoverable on replay, whereas
in-loop retry mid-shutdown is futile; permanent `fetch_failed`, other 4xx, and
size limits stay non-retryable so they cannot loop in the spool.
Telegram reuses the shared helper instead of a local classifier
(`isAbortError`/`isTransientNetworkError` live in core; the classification must
not drift). Adds permanent-`fetch_failed` regression coverage at the unit and
behavioral level. Media-group partial delivery (#55216) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single-message inbound media fetch failure was handled best-effort: warn
and let the update complete, so bot-core acked the spooled update. During a
restart window the warning send also failed and the document was silently
lost with no durable retry.
Classify transient media fetch failures (network/abort fetch_failed and
408/429/5xx HTTP) and record them as failed-retryable, so spooled-replay
updates are kept and re-driven by the ingress spool with backoff instead of
acked. Live updates still ack and warn; permanent failures (size limit, 4xx)
stay best-effort. The retry notice is suppressed on spooled replay to avoid
repeating it on each replay.
The durable-retry classifier deliberately diverges from media/fetch.ts
shouldRetryMediaFetch on shutdown aborts: in-loop retry of a shutdown abort
is futile, but durable re-spool must retry it -- that abort is the primary
restart-window loss vector. Media-group partial delivery (#55216) is left
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(imessage): shed emoji anywhere in poll-vote echo match
iMessage native poll options carry a trailing emoji ("Lobster 🦞 ") while the
agent echoes its vote with a leading one ("🦞 Lobster.") under message_tool_only
reply mode. normalizePollEchoText stripped only a leading emoji prefix, so
"lobster 🦞" never matched "lobster" and the redundant text leaked past the
poll_vote_echo guard (shipped in #98421).
Shed emoji anywhere across every class: pictographic, regional-indicator flags
(🇺🇸), subdivision-flag tag chars, ZWJ, skin-tone, and keycap sequences cleared
as a unit (so "1️⃣" -> "" not "1", while a plain "1"/"#"/"*" survives). Internal
punctuation stays so C#/C++/Node.js remain distinct; emoji-only labels normalize
to empty and the guard's existing empty-option check fails open. Re-exports
normalizePollEchoText with a focused unit test across every emoji class plus a
message-tool integration test for the emoji-suffixed-option case.
* fix(imessage): make poll-echo emoji regex lint-safe
ClawSweeper/CI check-lint flagged no-misleading-character-class: combining
marks (VS16, U+20E3), ZWJ, and skin-tone modifiers are misleading inside a
single character class. Rewrite the emoji strip as an alternation using
property escapes (\p{Regional_Indicator}, \p{Emoji_Modifier}) plus standalone
atoms so the same normalization behavior passes lint. Behavior unchanged;
poll-echo unit + integration tests still green.
* fix(imessage): preserve poll option emoji identity
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>