When a channel stop times out (e.g. during a Telegram API outage),
the channel enters recoveryStopTimedOut state. The health monitor's
subsequent start call would set restartPending and return without
actually starting the channel.
If the stuck stop never completes, the channel stays in limbo forever
with the health monitor retrying every cycle but never recovering.
Fix: when the health monitor retries recovery (recoveryStartRequested
already set), clean up the stuck task state and allow the channel to
start normally.
Closes#94008
(cherry picked from commit 8b4be2fdd4)
When a required-mode batch send fails mid-batch after an earlier payload
already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup
called failDelivery. failDelivery only bumps retryCount/lastError; it does
not advance recoveryState, so the entry stayed in send_attempt_started (set
earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart).
On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started
and calls reconcileUnknownQueuedDelivery. When adapter reconciliation
misreports not_sent (the message was actually sent, per the outbound send
ok / messageId evidence), the entry is replayed and the user receives a
duplicate.
Fix: when the error carries send evidence (OutboundDeliveryError with
sentBeforeError === true and platformSendStarted === true), call
markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the
entry to unknown_after_send, which drain already routes through
reconcileUnknownQueuedDelivery, preserving the entry for adapter
reconciliation rather than leaving it in send_attempt_started for replay.
When there is no send evidence (sentBeforeError === false), failDelivery
remains correct: nothing reached the channel, so retrying is safe.
This is a third duplicate path distinct from #89812 (mirror best-effort)
and #92274 (subagent-announce-delivery retry); it is the outbound/deliver
wrapper catch, which neither prior fix covers.
Tests:
- regression: two payloads, first succeeds, second throws; asserts
markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not.
- guard: no send evidence; failDelivery still called.
(cherry picked from commit 71422a9a5a)
Summary:
- The PR preserves `failure_alert_disabled === 0` as the enabled-with-defaults failure-alert state and adds focused codec roundtrip tests.
- PR surface: Source +2, Tests +54. Total +56 across 2 files.
- Reproducibility: yes. At source level, current main encodes `failureAlert: {}` with `failure_alert_disabled = 0`, then decodes it as `undefined` when all explicit alert option columns are null.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head bd9b2a1798.
- Required merge gates passed before the squash merge.
Prepared head SHA: bd9b2a1798
Review: https://github.com/openclaw/openclaw/pull/96615#issuecomment-4794949533
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Approved-by: takhoffman
(cherry picked from commit a21144d8a6)
* fix(openai): bound Codex OAuth token response body reads with readResponseWithLimit
Replace unbounded response.arrayBuffer() in postTokenForm with
readResponseWithLimit using a 1 MiB cap to prevent OOM from oversized
token endpoint responses. Add real node:http loopback server tests.
* fix(openai): wrap readResponseWithLimit result in Uint8Array for TS BodyInit compat
- Fixes TS2345: Buffer<ArrayBufferLike> not assignable to BodyInit
- Resolves check-prod-types, check-test-types, and
check-additional-extension-package-boundary CI failures
Ref. https://github.com/openclaw/openclaw/pull/99479
* test(openai): verify OAuth response release
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit a89fe705b8)
* fix(discord): bound gateway metadata response body reads to prevent OOM
The materializeGuardedResponse helper in the Discord gateway metadata path
buffered the full upstream Response body via response.arrayBuffer() without
any size cap. A malicious or malfunctioning /gateway/bot endpoint that returns
an oversized payload could exhaust gateway memory.
Replace arrayBuffer() with readResponseWithLimit(4 MiB), consistent with
DISCORD_API_RESPONSE_BODY_LIMIT_BYTES in api.ts. Overflow throws an Error
with the byte counts.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(discord): wrap readResponseWithLimit result in Uint8Array for type compat
readResponseWithLimit returns Buffer which is not assignable to BodyInit in
the undici Response constructor type. Wrap in new Uint8Array() to satisfy the
boundary dts check. Also remove testExports export and mock fetchWithSsrFGuard
in tests via vi.hoisted + vi.mock to avoid leaking internal test-only exports.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(discord): tighten gateway metadata proof
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit fc77c2b04b)
* fix(gateway): truncateCloseReason drops partial UTF-8 code point instead of emitting mojibake
Buffer.subarray at a raw byte offset can cut inside a multi-byte UTF-8
sequence. Node decodes the dangling continuation bytes as U+FFFD (3 bytes
each), so the re-encoded result can exceed the intended maxBytes cap —
violating the RFC 6455 close-reason size contract and surfacing garbled
text to clients.
Back up from the cut point to the nearest UTF-8 sequence start before
slicing (UTF-8 continuation bytes match 10xxxxxx; skip them).
Closes#99976
* fix(lint): remove unnecessary non-null assertion on Buffer index
* ci: retrigger checks
(cherry picked from commit 161c4581a7)
* fix(infra): close fd and remove lock file on writeFile failure (#98958)
When fs.open(lockPath, "wx") succeeds but handle.writeFile() fails
(e.g. disk full / ENOSPC), close the file handle and remove the
partially-written lock file before re-throwing to avoid a file
descriptor leak and stale lock artifact.
Changes:
- src/infra/gateway-lock.ts: nested try-catch around writeFile
- src/infra/gateway-lock.test.ts: test for fd close + lock cleanup
- scripts/verify-gateway-lock-fd-leak*.mjs: fault-injection proof
Co-Authored-By: Claude <noreply@anthropic.com>
* test(infra): strengthen gateway lock cleanup proof
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 36dd9ee3c3)
* fix(gateway): cap agentRunCache to prevent unbounded growth under run fan-out
Time-based prune only reclaims entries past the 10-minute TTL window; a burst
of run fan-out can add far more entries than the window reclaims, so the cache
could grow without bound between prunes. Add a FIFO entry cap (5000) enforced
on insert, mirroring the existing Discord REST entity-cache bound.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(gateway): preserve waited run snapshots under cache cap
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
(cherry picked from commit f35fbc898c)
Replace unbounded res.json() with readProviderJsonResponse in the
fetchClaudeUsage error path to cap error body reads at 16 MiB.
(cherry picked from commit 615558f6fb)
* fix(copilot): redact OAuth error response body in fetchJson error messages
Replace raw response body text with bounded, redacted structured error detail
extracted via extractProviderErrorDetail so OAuth error responses containing
tokens, device codes, or other sensitive fields are not leaked into Error
messages and downstream logs.
* test(copilot): add device-code and non-JSON error body redaction cases
Add regression tests covering the device code flow and non-JSON error
bodies alongside the existing token refresh coverage. Also include live
proof output showing real Response object redaction via
extractProviderErrorDetail.
Ref: #102953
* fix(copilot): normalize OAuth HTTP failures
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 6b90610cdb)
* fix(oauth): bound github-copilot OAuth response reads at 16 MiB
* chore: trigger CI re-run after PR body update
(cherry picked from commit bd0c052aa5)
The Discord REST main response path read the body with an unbounded
await response.text() before JSON-parsing it. A controlled or hijacked
endpoint could stream an arbitrarily large body and exhaust memory (OOM).
Wrap the read in the canonical readResponseWithLimit helper with an 8 MiB
cap (well above any legitimate Discord JSON payload) plus an idle timeout
tied to the request timeout, so the stream is cancelled at the cap or on
stall instead of buffering unbounded. Normal payloads still parse fully.
This mirrors PR #95108 which bounded the analogous Anthropic Messages
error-response read with the same helper.
(cherry picked from commit 2d2a50c00d)
The 64 KiB inter-event SSE sanitize buffer added in #96989 rejects a single
legitimate event larger than 64 KiB — e.g. a large gpt-5.5 reasoning summary on
the openai-chatgpt-responses API — throwing "SSE response exceeded max buffer
size (65536 bytes) without event boundary" and failing the whole request. The
default ChatGPT-subscription gpt-5.5 path is unusable (present in v2026.6.11-beta.1).
Decouple the two bounds: keep the non-OK error-body cap tight at 64 KiB
(SSE_NONOK_BODY_MAX_BYTES), and raise the inter-event sanitize buffer to the same
16 MiB ceiling as the JSON-synthesis path. The guard still trips on a genuinely
boundary-less (hostile/broken) stream, just not on a real large event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 81d60ca30d)
* fix(provider-transport-fetch): bound SSE buffer to prevent OOM
* fix(provider-transport-fetch): appease oxlint curly rule in test
* fix(provider-transport-fetch): drain events before cap + cancel reader on overflow
* fix(provider-transport-fetch): remove unused encoder from coalesced chunk test
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(transport): tighten SSE buffer guards
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
(cherry picked from commit 1bccd29304)
A present-but-unreadable openclaw.json (for example EACCES after a sudo
command leaves it root-owned) returns an empty best-effort fallback
snapshot. A later full-file write (openclaw doctor, including the update
doctor pass) then serialized a skeletal config over the still-rich file,
dropping gateway.mode and bricking gateway startup.
The fallback base has no raw bytes and an empty resolved config, so the
existing size-drop and gateway-mode-removed guards never fired, and the
update path passes allowConfigSizeDrop=true.
Record the read failure on the snapshot (readError) and treat an
unreadable base as an always-blocking write reason
(unreadable-config-before-write) that allowConfigSizeDrop does not bypass.
The allowDestructiveWrite escape hatch and the rejected-artifact path are
preserved, so explicit recovery still works and the blocked payload is
saved to openclaw.json.rejected.<timestamp>.
Refs #78493.
(cherry picked from commit 14198836b2)
* fix(config/sessions): narrow reply-session initialization revision to identity fields
The initialization guard compared the full persisted session entry, so
background touches to updatedAt, heartbeat timestamps, context-budget
metadata, etc. produced false-positive stale-snapshot conflicts and the
"reply session initialization conflicted" error.
Only sessionId and sessionFile matter for detecting a session rotation.
Narrow the revision to those identity fields and add a regression test.
Fixes#98672
* fix(config/sessions): merge current metadata when reply-init identity guard passes
* fix(config/sessions): preserve only snapshot-drifted metadata in reply-init commit
* fix: preserve cleared reply-session metadata
* fix: allow same-session reply initialization drift
---------
Co-authored-by: moguangyu5-design <moguangyu5-design@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
(cherry picked from commit 826c84ea19)