Route success JSON reads through readProviderJsonResponse (16 MiB cap) in
azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and
openrouter/media-understanding to prevent OOM from oversized or hostile
endpoint responses. Mirrors the response-limit campaign already applied to
other provider paths.
AI-assisted.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): prefer desktop app-server for Computer Use on macOS
* fix(codex): fall back from stale desktop app-server
---------
Co-authored-by: Benjamin Badejo <ben@benbadejo.com>
The Copilot usage read in extensions/github-copilot/usage.ts parsed its
HTTP response with an unbounded await res.json(). A hostile or buggy
api.github.com proxy (the proxy endpoint is derived from a user-supplied
token) could stream an unbounded JSON body and drive the usage snapshot
into OOM.
Route the read through the shared readProviderJsonResponse (from
openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap,
cancels the stream on overflow, and wraps malformed JSON with the caller
label. Same no-helper-import-to-bounded-reader shape as the #96027 /
#96038 response-limit work.
Add a focused regression test: when the usage stream exceeds the JSON
byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the
reader cancels the body mid-flight instead of buffering the full
advertised stream. Existing parse/HTTP-error cases keep passing.
The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.
Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.
Symmetric counterpart to the #96027/#96038 response-limit campaign.
Keep reserved-target detection behavior unchanged while routing callers through a shared helper so future changes stay localized.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
In resolveAgentDeliveryPlanWithSessionRoute, reserved-literal errors from the
sync outbound target check are no longer treated as fatal. Instead, the path
proceeds to resolveOutboundSessionRoute which calls resolveMessagingTarget,
already fixed to do directory-first lookup before rejecting reserved literals.
This preserves configured Telegram directory entries named like reserved words
(current, self, this, me) through the explicit agent/gateway delivery path.
Update docs to reflect directory-first ordering.
Move the reserved-literal check from before directory lookup to after directory
miss, so configured Telegram groups/channels whose directory key is a reserved
word (current, self, this, me) still resolve through the directory before
failing closed. The reserved check now runs only after the directory returns no
match and before plugin fallback resolution.
Update the regression test to verify directory-first ordering: a configured
directory entry named current resolves successfully, and a directory miss with
a reserved literal fails with the descriptive error.
The sync reserved-literal check in resolveOutboundTargetWithPlugin was
suppressing heartbeat routes to directory entries whose names match
reserved literals (e.g., a Telegram group named "current"). Skip the
check for heartbeat mode so the async resolveChannelTarget →
resolveMessagingTarget path can do directory-first lookup before
deciding.
readSessionBranch filtered out every entry lacking a string or number
timestamp. Sessions written before entry timestamps existed (version 1)
have ids and parentIds synthesized by the legacy migration but no entry
timestamp, so all entries were dropped and the exported bundle reported
transcriptEventCount 0. The transcript event builder already defaults a
missing timestamp via normalizeTimestamp, so the filter clause was both
wrong and redundant. Drop it; entry identity plus the canonical-entry
check is what the branch walk needs.
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
- Add integration test: after mid-batch failure with send evidence the
resulting unknown_after_send entry is NOT replayed by reconnect drain
when no adapter reconciliation is available ('refusing blind replay').
Pins the drain contract so any regression that re-enables blind replay
is caught end-to-end against a real SQLite queue.
- Add comment in deliver.ts fallback branch: failDelivery inside the
markQueuedPlatformOutcomeUnknown catch is a last-resort DB-write-error
path, not an indication that failDelivery is correct with send evidence.
Complement the unit test (which mocks delivery-queue) with an integration
test that uses the real SQLite delivery queue (no mock of ./delivery-queue.js)
and the real deliverOutboundPayloads code path.
Verifies at the queue layer:
- mid-batch failure with send evidence (first payload succeeds, second
throws, queuePolicy=required) -> queue entry recovery_state advances to
unknown_after_send, retryCount stays 0, no lastError. This is the patch
path: drain will route the entry through reconcileUnknownQueuedDelivery
instead of leaving it in send_attempt_started for blind replay.
- no send evidence (sole payload fails immediately) -> failDelivery path:
retryCount bumped, recovery_state stays send_attempt_started. Patch does
not affect this path.
Negative control confirmed: with deliver.ts reverted to v2026.6.8 original
(no patch), the mid-batch test fails with recovery_state=send_attempt_started
(the root-cause state), while the no-evidence test still passes. This
reproduces the patch's code path and proves the fix at the real-queue layer.
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.