* fix(auto-reply): treat U+2028/U+2029 as paragraph boundaries when chunking
chunkByParagraph normalized only CR/CRLF before blank-line paragraph detection,
so model output using Unicode LINE/PARAGRAPH SEPARATOR (U+2028/U+2029) instead
of a blank line was not split at those boundaries and fell back to length-based
splitting. Normalize U+2028/U+2029 to \n alongside CR/CRLF, matching how the
Control UI markdown renderer handles them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-reply): normalize U+2028 as line break, U+2029 as blank-line paragraph boundary
Distinguish U+2028 (LINE SEPARATOR) from U+2029 (PARAGRAPH SEPARATOR):
U+2029 becomes \n\n (blank line — paragraph boundary) while U+2028
becomes \n (single newline — intra-paragraph line break).
The original fix mapped both to \n, so standalone U+2029 still
produced single-line text without a blank-line gap — paragraph
detection failed. The combined U+2028 input accidentally
produced the right blank-line sequence, which masked the bug.
Adds individual tests for lone U+2029 (splits at paragraph boundary),
lone U+2028 (stays within paragraph), and consecutive U+2028
(combined blank line — matches \n\n behavior).
* test(auto-reply): simplify Unicode separator cases
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 3a4e2ef65f)
* fix(discord): honor caller abortSignal during 429 retry backoff
* test(discord): prove 429 backoff abort through a real loopback server
* test(discord): make retry abort proof deterministic
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 40acbb05c9)
* fix(msteams): cancel non-OK consent upload response body before throwing
* fix(msteams): release all consent upload responses
Cancel unread response bodies after successful uploads as well as failed uploads, and fold the lifecycle assertions into the existing status-path tests.
Co-authored-by: Monkey-wusky <66244686+Monkey-wusky@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 9b238a7927)
* fix(proxy-capture): guard body-less arrayBuffer reads against oversized responses
* fix(proxy-capture): exercise body-less fallback in bounded read tests
New tests use mock clones with body: null plus arrayBuffer spies to
prove the content-length precheck guards the !body path. A real
Response clone exposes body.getReader in Node 24, so the prior test
only exercised the streaming branch and would stay green even if
the precheck were deleted.
* chore: retrigger CI
* fix(proxy-capture): reject non-safe content-length before arrayBuffer (#101268)
ClawSweeper P2: the body-less fallback used Number(content-length), so a
huge digit-only Content-Length value could overflow to Infinity, bypass the
Number.isFinite guard, and still call arrayBuffer() — leaving an OOM path in
the hardening PR.
Add declaredContentLengthExceedsCap, which accepts only plain digit strings,
treats any value longer than Number.MAX_SAFE_INTEGER as oversized, and
compares safe-integer parsed values against the cap. Non-numeric or malformed
values fall through to the post-read length check.
Adds a regression test for a 100-digit Content-Length that would previously
have bypassed the guard.
* fix(proxy-capture): normalize zero-padded Content-Length before digit-count guard
* fix(proxy-capture): fail closed without response streams
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 7cde0ac8c0)
* fix(qqbot): bound tail log reads to actual bytes returned by fs.readSync
* fix(qqbot): satisfy knip deadcode check for testing export
Add __testing re-export and test-api.ts barrel so knip traces the testing export through a recognized entry point.
* fix(qqbot): restore testing export alongside __testing re-export
Both exports are needed: testing for proof scripts, __testing for knip tracing.
* fix(qqbot): remove unused __testing re-export from log-helpers
test-api.ts already imports testing and re-exports as __testing. The extra re-export in log-helpers.ts was unused by production code.
* fix(qqbot): retry short log tail reads
* test(qqbot): keep short-read seam private
Co-authored-by: RileyJJY <100176083+RileyJJY@users.noreply.github.com>
* test(qqbot): exercise short reads through log export
Co-authored-by: RileyJJY <0668000974@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: RileyJJY <100176083+RileyJJY@users.noreply.github.com>
(cherry picked from commit 3f89aae98d)
* fix(agents): bound base64 image input before decode in tool-image sanitizer
* fix(agents): lower input-size cap to 10MB for OOM headroom
* fix(agents): align tool-image input-cap comment with 10 MiB ceiling
* fix(agents): typecheck-safe access in tool-image input-cap test
* test(agents): exercise real tool image input cap
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit b85531c62f)
* fix(memory-core): write MEMORY.md atomically during short-term promotion
applyShortTermPromotions rewrote MEMORY.md with a single non-atomic
fs.writeFile, which truncates the file before streaming the new content.
An OS write failure part way through (for example EFBIG on a size-limited
or full volume) left MEMORY.md truncated to the bytes written before the
failure, permanently dropping user long-term memory. The dreaming cron
path invokes this writer automatically, and the recall store is only
updated after the write, so the promotion stays eligible and the next
run reads the already-truncated file.
Route the write through replaceFileAtomic (temp file, fsync, atomic
rename), the same durable-write helper the sibling DREAMS.md writer in
this extension already uses. On failure the temp file is discarded and
the existing MEMORY.md is left untouched; on success the content and the
existing file mode are preserved.
* fix(memory-core): harden atomic promotion durability
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 0317d7e628)
* fix(sessions): stop leaking file path as prompt content on read failure
When readFileSync fails for a valid file path, resolvePromptInput returns
the raw path string as prompt content instead of undefined. This injects
filesystem paths into the LLM context. The existing console.error warning
still fires; the caller already handles undefined returns correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: cover unreadable prompt paths
* test: use tracked resource loader temp dirs
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit c924819292)
* fix(synology-chat): settle user_list overflow without hanging
Cap reads at 1 MiB with Buffer concat. On overflow, finish the promise
before destroy() — bare destroy often skips end/error and hung the test.
* fix(synology-chat): share bounded user-list reader
Co-authored-by: zw-xysk <zhao.wang1@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 30c257f6b4)
* fix(moonshot): bound video description JSON response reads
The Moonshot video description endpoint used an unbounded await res.json()
to parse the media understanding response. Route through
readProviderJsonResponse (16 MiB cap) to match the bound already in
place for other media understanding providers (xai, openrouter).
AI-assisted.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(moonshot): add bounds and malformed-JSON coverage for video description
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 765d05c2e4)
Replace unbounded response.text() in readResponseBody and
response.json() in the device-code polling loop with
readResponseWithLimit (16 MiB cap).
(cherry picked from commit d5aca1d6d2)
Replace bare `await response.json()` in `installSignalCliFromRelease` with
`readProviderJsonResponse` (16 MiB cap, stream cancel on overflow). The
external GitHub Releases endpoint can include a large `body` changelog field;
the error path was already guarded but the success path was unbounded.
The existing inner catch continues to convert overflow errors into the
graceful `{ ok: false, error: "Failed to parse signal-cli release info." }` path.
Adds a regression test verifying the stream is cancelled before all chunks are
read on an oversized 20 MiB streaming response.
Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 51064bda4d)
Route OpenRouter video submit and poll success JSON through the shared bounded provider JSON reader, preserving malformed-response mapping and SSRF request policy coverage.
(cherry picked from commit 48f34b1d4d)
* fix(minimax): bound image/video success response reads
MiniMax image generation and video generation (task submit + status poll)
read their success responses through unbounded `await response.json()`, so
a misbehaving or hostile endpoint could stream an arbitrarily large body
into memory before parsing and exhaust the process. Read those success
bodies through the shared bounded reader (16 MiB cap, the same limit other
bundled providers and the sibling MiniMax web-search provider already use)
and cancel the stream on overflow. The error-body path is already bounded
via assertOkOrThrowHttpError; this closes the matching success-JSON gap.
MiniMax TTS is already bounded and is left unchanged.
AI-assisted.
* fix(minimax): bound video metadata response reads
* fix(minimax): leave image response sizing to image hardening
* fix(minimax): bound image/video success response reads
MiniMax image generation and video generation (task submit + status poll)
read their success responses through unbounded `await response.json()`, so
a misbehaving or hostile endpoint could stream an arbitrarily large body
into memory before parsing and exhaust the process. Read those success
bodies through the shared bounded reader (16 MiB cap, the same limit other
bundled providers and the sibling MiniMax web-search provider already use)
and cancel the stream on overflow. The error-body path is already bounded
via assertOkOrThrowHttpError; this closes the matching success-JSON gap.
MiniMax TTS is already bounded and is left unchanged.
AI-assisted.
* fix(minimax): bound video metadata response reads
(cherry picked from commit 25e184aeab)
* fix(mattermost): bound successful REST JSON/text response reads
The Mattermost REST client already bounds error bodies
(readResponseTextLimited) and streams guarded responses without buffering,
but the success path still called `await res.json()` / `await res.text()`,
reading the whole body into memory before parsing. A self-hosted or
compromised Mattermost server can return an arbitrarily large (or
never-terminating, content-length-less) JSON/text body and force the plugin
to buffer it unbounded.
Read successful JSON through the shared readProviderJsonResponse (16 MiB cap,
cancels the stream and throws a bounded error on overflow, same as the
provider HTTP path) and cap non-JSON success bodies with readResponseTextLimited.
uploadMattermostFile's file-info JSON is bounded the same way.
Symmetric follow-up to the #95103 / #95108 response-limit campaign.
AI-assisted.
* fix(mattermost): bound probe success JSON reads
* fix(mattermost): reject oversized success text bodies
(cherry picked from commit 9241b9701d)