Skip to content

fix(tlon): bound Urbit SSE stream reads at 16 MiB#97687

Closed
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/tlon-sse-bound
Closed

fix(tlon): bound Urbit SSE stream reads at 16 MiB#97687
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/tlon-sse-bound

Conversation

@wangmiao0668000666

Copy link
Copy Markdown
Contributor

What Problem This Solves

extensions/tlon/src/urbit/sse-client.ts:237 (the UrbitSSEClient.processStream body) reads the Urbit /~/channel/<id> SSE response in an unbounded for await (const chunk of stream) loop, accumulating chunk.toString() into a single buffer until the stream completes. A hostile or malfunctioning Urbit node can return an SSE body of arbitrary size — the runtime has no upper bound on memory consumption for this read path. There is no fallback reader that enforces a cap before the consumer sees the bytes.

This is the same class of bug closed for core LLM SSE providers in #96701, #96723, #96972, #96989, #96772, #97628, and #97648. This PR closes it for the Tlon Urbit channel's SSE client.

Why This Change Was Made

The bounded-read helper readByteStreamWithLimit is already exported from openclaw/plugin-sdk/response-limit-runtime (re-exported from @openclaw/media-core/read-byte-stream-with-limit). It accepts an AsyncIterable<unknown> (both Node ReadableStream and the result of Readable.fromWeb(webStream) qualify), tracks accumulated bytes against a hard cap, throws a labeled error on overflow, and destroy()s the underlying stream so producers stop sending. The tlon extension already imports other helpers from openclaw/plugin-sdk/* (ssrf-runtime, number-runtime) — using response-limit-runtime here is consistent with the established plugin boundary (no core internals, no relative outside-package imports).

The existing chunk-parse loop's \n\n boundary scan is preserved verbatim after the bounded read — only the data source changes from "streamed chunks" to "bounded Buffer". Event dispatch semantics are identical.

The cap of 16 MiB matches the shared PROVIDER_TEXT_RESPONSE_MAX_BYTES / PROVIDER_JSON_RESPONSE_MAX_BYTES convention used in src/agents/provider-http-errors.ts and the cap used in the OAuth bounded-read PRs (#97628, mine).

User Impact

  • Existing successful Tlon/Urbit sessions keep working unchanged — same event dispatch, same ack tracking, same reconnection behavior.
  • New failure mode: tlon Urbit SSE: body exceeds 16777216 bytes (got <size>) if the SSE body exceeds 16 MiB. This is the desired safety behavior — the channel does not silently buffer gigabytes before the caller sees the error.
  • Single-file change in the production code (extensions/tlon/src/urbit/sse-client.ts); no API, config, migration, or docs/** change.

Changes

  • extensions/tlon/src/urbit/sse-client.ts:8 — add import { readByteStreamWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; (alphabetical position).
  • extensions/tlon/src/urbit/sse-client.ts:11 — add const TLON_SSE_BODY_MAX_BYTES = 16 * 1024 * 1024; with WHY-comment naming the cap.
  • extensions/tlon/src/urbit/sse-client.ts:228-272 — replace processStream's for await (chunk) unbounded accumulator with readByteStreamWithLimit(stream, {maxBytes: TLON_SSE_BODY_MAX_BYTES, onOverflow: ...}) followed by the existing \n\n boundary scan on buffer.toString("utf8"). WHY-comment explains the 16 MiB cap rationale and points to the sibling bounded-read PRs.
  • extensions/tlon/src/urbit/sse-client.bounded.test.ts — new file, 2 inline tests via loopback http.createServer (see ## Evidence for breakdown).

No SDK barrel change, no API/config/migration change, no docs/** change, no package.json change, no sibling-file change.

Evidence

This section is a structured, verifiable proof that the change works. All commands and outputs are reproducible from a clean clone of fix/tlon-sse-bound at the head SHA below.

Pre-flight gate

# Check Result
0 Sibling PR check: gh search prs --repo openclaw/openclaw --state open "tlon" Only #97558 (NarahariRaghava) which covers JSON reads via readProviderJsonResponse in tlon-api.ts / channel-ops.tsdoes NOT touch sse-client.ts (verified via gh pr view 97558 --json files). No SSE overlap.
1 Diff scope: git diff --numstat HEAD~1 extensions/tlon/src/urbit/sse-client.ts +22/-12, new file sse-client.bounded.test.ts +116/-0. Prod LoC +10 / Test LoC +116.
2 PR-template dry-run (run after PR creation)

Two-layer proof (per loopback-proof-required-bounded-read.md memory)

Layer 1: Real-wire loopback proof — oversized body cap fires

A real http.createServer listening on 127.0.0.1:0 streams 18 × 1 MiB chunks via setInterval(..., 1) with Content-Type: text/event-stream. Real global fetch() is consumed by UrbitSSEClient.processStream. Cap firing is observed end-to-end on actual TCP framing.

stdout | UrbitSSEClient processStream bounded-read real wire proof > caps an oversized body streamed chunked over real wire
[tlon SSE bounded-read proof] oversized path: cap=16777216 reported=16829788 server_total=18874368
 ✓ |extension-messaging| UrbitSSEClient processStream bounded-read real wire proof > caps an oversized body streamed chunked over real wire 272ms

Layer 2: Real-wire loopback proof — normal-size body events dispatch

Same loopback pattern with a small SSE-shaped body containing 2 events. Events fire through the registered handler correctly.

stdout | UrbitSSEClient processStream bounded-read real wire proof > returns and dispatches events for normal-size SSE body on real wire
[tlon SSE bounded-read proof] normal path: events received=2
 ✓ |extension-messaging| UrbitSSEClient processStream bounded-read real wire proof > returns and dispatches events for normal-size SSE body on real wire 10ms

Quantitative read of Layer 1

metric value interpretation
configured cap (MAX) 16 777 216 bytes (16 MiB) matches TLON_SSE_BODY_MAX_BYTES
reported at throw (loopback, run shown) 16 829 788 bytes cap (16 777 216) + 52 572 bytes of TCP-coalesced packet after cap — proves cap fired (got > MAX) and we did not buffer beyond the next available read (got < TOTAL)
server full body (TOTAL) 18 874 368 bytes (18 MiB) confirms server emitted the full 18 MiB (cap was not triggered by upstream truncation)
test invariant MAX < got < TOTAL holds across runs; TCP coalescing on 127.0.0.1 produces a fixed-but-non-deterministic reported size after cap
normal path 2 events received, exact dispatch match the existing \n\n boundary scan consumes the bounded Buffer correctly; event handlers fire as before

Combined evidence

  • node scripts/run-vitest.mjs run extensions/tlon/src/urbit/sse-client.test.ts extensions/tlon/src/urbit/sse-client.bounded.test.ts18/18 tests pass (16 existing + 2 new bounded-read tests) in 3.35s.
  • node scripts/run-tsgo.mjs -p tsconfig.extensions.json — exit 0 (no type errors).
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions/tlon/src/urbit/sse-client.ts extensions/tlon/src/urbit/sse-client.bounded.test.ts — exit 0 (no lint errors, no floating-promise warnings).

Reproduce locally:

git checkout fix/tlon-sse-bound
node scripts/run-vitest.mjs run extensions/tlon/src/urbit/sse-client.bounded.test.ts --reporter=verbose   # 2/2 pass with loopback proof stdout
node scripts/run-tsgo.mjs -p tsconfig.extensions.json   # exit 0
node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json \
  extensions/tlon/src/urbit/sse-client.ts extensions/tlon/src/urbit/sse-client.bounded.test.ts   # exit 0

Layer 1 wire details (so reviewers can re-derive the byte counts):

  1. http.createServer listens on 127.0.0.1:0 (random port) and chunks 18 × 1 MiB = 18 874 368 bytes via setInterval(..., 1) with Content-Type: text/event-stream.
  2. Real fetch("http://127.0.0.1:<port>/") returns a Response whose body is a ReadableStream<Uint8Array> from the underlying undici socket.
  3. client.processStream(body) calls Readable.fromWeb(body) (Node Readable) and hands it to readByteStreamWithLimit(stream, {maxBytes: 16 777 216, onOverflow: ({size}) => Error(...)}).
  4. readByteStreamWithLimit accumulates chunks via for-await. After total + nextChunk.byteLength > 16 MiB, the helper calls destroy() on the Readable and throws the labeled error. The exact reported size depends on TCP coalescing at moment-of-throw — hence the invariant MAX < got < TOTAL rather than an exact byte assertion.
  5. The error propagates up through processStream to the caller's .catch(...) (line 213 in sse-client.ts); the existing streamRelease finally-block still runs, the streamController is nulled, and auto-reconnect logic is preserved.

Related

Label: security

AI-assisted.

Urbit SSE client processStream() previously did an unbounded
for-await loop on the channel response body. A hostile or broken
Urbit node could return an unbounded event stream and exhaust
OpenClaw memory before any caller-side guard fires.

Replace the chunked buffer accumulation with readByteStreamWithLimit
from openclaw/plugin-sdk/response-limit-runtime at 16 MiB cap,
matching the JSON/OAuth response cap pattern. Events are then
parsed from the bounded buffer via the existing indexOf("\n\n")
boundary scan, preserving event-dispatch semantics.

The cap fires with a labeled error:
  tlon Urbit SSE: body exceeds 16777216 bytes (got <size>)

Companion pattern to src/llm/providers/mistral.ts (openclaw#97648) and
src/llm/providers/anthropic.ts (openclaw#96701/openclaw#96723) for non-extension
provider SSE bounds. This is the first extension-side SSE bound.

Co-Authored-By: Claude <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added channel: tlon Channel integration: tlon size: S labels Jun 29, 2026
@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 29, 2026, 2:41 AM ET / 06:41 UTC.

Summary
The PR adds a 16 MiB bounded read to UrbitSSEClient.processStream and adds loopback tests for oversized and finite normal SSE bodies.

PR surface: Source +10, Tests +112. Total +122 across 2 files.

Reproducibility: yes. Current main’s unbounded accumulation is source-reproducible in processStream, and the PR regression is also source-reproducible because readByteStreamWithLimit returns only after the stream ends.

Review metrics: 1 noteworthy metric.

  • Streaming parser mode: 1 long-lived SSE consumer changed to whole-body read. The measured behavior change matters because Urbit channel delivery depends on dispatching frames while the response remains open.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦪 silver shellfish
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Replace the whole-body read with an incremental byte cap in the existing parse loop.
  • [P1] Add a test or loopback proof where the server sends one event and keeps the SSE connection open while the handler is observed firing.
  • [P1] Rerun the targeted Tlon SSE tests, extension typecheck, and focused lint after the repair.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR body includes loopback live output for oversized and finite normal bodies, but it does not show after-fix event delivery while an SSE response remains open; add proof after repairing the parser and redact private endpoints or tokens. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging this would stall Tlon message delivery because events from the long-lived SSE response are parsed only after the response body ends or exceeds 16 MiB.
  • [P1] The submitted real behavior proof covers finite loopback responses, but not event dispatch while the SSE connection remains open.

Maintainer options:

  1. Repair the streaming parser before merge (recommended)
    Keep the byte cap but implement it as a per-chunk guard or bounded rolling buffer so events dispatch before the SSE response closes.
  2. Require open-stream proof after repair
    After the parser is fixed, require loopback or live-output proof that an event is delivered while the server keeps the SSE response open.
  3. Pause this branch if it keeps whole-body reads
    If the approach remains a finite-body reader, the branch should not land because it changes the channel’s core delivery semantics.

Next step before merge

  • [P1] Contributor repair and updated real behavior proof are needed because the patch changes streaming semantics and the external PR proof misses the open-stream delivery path.

Security
Needs attention: The patch is security-motivated, but the current implementation creates a concrete channel availability and delivery regression on the live SSE path.

Review findings

  • [P1] Keep SSE event dispatch incremental — extensions/tlon/src/urbit/sse-client.ts:244-248
Review details

Best possible solution:

Bound bytes incrementally inside processStream while preserving the existing per-chunk processEvent dispatch as each \n\n frame arrives.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main’s unbounded accumulation is source-reproducible in processStream, and the PR regression is also source-reproducible because readByteStreamWithLimit returns only after the stream ends.

Is this the best way to solve the issue?

No. A finite whole-body bounded reader is the wrong layer for a long-lived SSE parser; the maintainable fix is an incremental byte cap that preserves streaming dispatch.

Full review comments:

  • [P1] Keep SSE event dispatch incremental — extensions/tlon/src/urbit/sse-client.ts:244-248
    This awaits readByteStreamWithLimit before parsing any events. That helper reads the whole async iterable and returns only after done, but /~/channel/<id> is a long-lived SSE response; normal Tlon messages will not be delivered until the connection closes or the cap fires. Keep the byte cap inside the existing chunk loop so each complete \n\n frame still reaches processEvent immediately.
    Confidence: 0.97

Overall correctness: patch is incorrect
Overall confidence: 0.96

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 05cf49776766.

Label changes

Label changes:

  • add P2: The PR targets a real bounded-read security/availability hardening gap in one bundled channel plugin, but the remaining work is scoped to one parser path.
  • add merge-risk: 🚨 message-delivery: The diff delays Tlon SSE event parsing until stream completion, which can suppress normal message delivery on an open firehose stream.
  • add merge-risk: 🚨 availability: The long-lived channel stream can now sit buffered until close or overflow, causing a stalled or reconnecting Tlon runtime instead of steady event processing.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body includes loopback live output for oversized and finite normal bodies, but it does not show after-fix event delivery while an SSE response remains open; add proof after repairing the parser and redact private endpoints or tokens. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P2: The PR targets a real bounded-read security/availability hardening gap in one bundled channel plugin, but the remaining work is scoped to one parser path.
  • merge-risk: 🚨 message-delivery: The diff delays Tlon SSE event parsing until stream completion, which can suppress normal message delivery on an open firehose stream.
  • merge-risk: 🚨 availability: The long-lived channel stream can now sit buffered until close or overflow, causing a stalled or reconnecting Tlon runtime instead of steady event processing.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body includes loopback live output for oversized and finite normal bodies, but it does not show after-fix event delivery while an SSE response remains open; add proof after repairing the parser and redact private endpoints or tokens. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +10, Tests +112. Total +122 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 22 12 +10
Tests 1 112 0 +112
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 134 12 +122

Security concerns:

  • [medium] Whole-body SSE read stalls the live channel — extensions/tlon/src/urbit/sse-client.ts:244
    Using a finite-body read helper on the Urbit SSE firehose prevents event handling until the stream ends or overflows, creating an availability failure for normal Tlon channel operation.
    Confidence: 0.94

What I checked:

  • Repository policy read: Read the full root policy and scoped extensions policy; the plugin boundary allows openclaw/plugin-sdk/* imports, but review policy requires checking the whole streaming behavior and best-fix shape, not just the diff. (AGENTS.md:13, 05cf49776766)
  • Scoped extension policy: extensions/AGENTS.md says extension production code should import through openclaw/plugin-sdk/*, which this PR does, so the blocker is behavioral rather than an owner-boundary import violation. (extensions/AGENTS.md:8, 05cf49776766)
  • Current streaming behavior: Current processStream parses and dispatches complete \n\n SSE events inside the chunk loop before the response body ends. (extensions/tlon/src/urbit/sse-client.ts:237, 05cf49776766)
  • PR changes streaming to whole-body read: The diff awaits readByteStreamWithLimit(stream, ...) before converting to text and scanning events, so event parsing cannot start until the async iterable completes or throws. (extensions/tlon/src/urbit/sse-client.ts:244, 245c30d5ca92)
  • Helper contract is finite-body buffering: readByteStreamWithLimit reads and concatenates the entire async byte stream, then returns Buffer.concat(...) only after the for await loop finishes. (packages/media-core/src/read-byte-stream-with-limit.ts:49, 05cf49776766)
  • Runtime caller is long-lived SSE: openStream opens /~/channel/<id> with Accept: text/event-stream and starts processStream(response.body) asynchronously; the monitor registers subscriptions and then connects this firehose stream. (extensions/tlon/src/urbit/sse-client.ts:185, 05cf49776766)

Likely related people:

  • arthyn: Authored the major Tlon channel/plugin update that restored sse-client.ts with Urbit channel SSE behavior and ack tracking. (role: introduced behavior; confidence: high; commits: f4682742d9d1; files: extensions/tlon/src/urbit/sse-client.ts, extensions/tlon/src/monitor/index.ts)
  • steipete: Recent commits on this file include SSE reconnect delay clamping, malformed event-id handling, helper export trimming, and shared channel request refactors. (role: recent area contributor; confidence: high; commits: 65167c963733, 8b180fe829b7, 5af8322ff58f; files: extensions/tlon/src/urbit/sse-client.ts, extensions/tlon/src/urbit/channel-ops.ts)
  • vincentkoc: Recent Tlon SSE and plugin-boundary commits wrapped malformed SSE JSON and repaired plugin/runtime seam drift around the same extension. (role: adjacent maintainer history; confidence: medium; commits: c59e9009039d, 0e54440ecc39, 5765c4cb2a87; files: extensions/tlon/src/urbit/sse-client.ts, extensions/tlon/src/urbit/channel-ops.ts)
  • steipete: Recent history on packages/media-core/src/read-byte-stream-with-limit.ts traces the helper extraction/documentation to media-core package work, which is the dependency contract this PR relies on. (role: bounded-read helper owner; confidence: medium; commits: 77f1359612f6, 0b8aabe8648e; files: packages/media-core/src/read-byte-stream-with-limit.ts, src/plugin-sdk/response-limit-runtime.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 29, 2026
@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

Closing this PR after rereading the ClawSweeper review, the @extensions/tlon/src/urbit/sse-client.ts:244 finding, and the underlying readByteStreamWithLimit contract.

The reviewer is correct: this change has a fundamental design flaw and should not land in its current shape.

What the patch actually does

readByteStreamWithLimit (in packages/media-core/src/read-byte-stream-with-limit.ts:49) is a finite-body buffer: it iterates the async iterable with for await, concatenates chunks, and only returns after done. Handing it processStream(response.body) for a long-lived Urbit /~/channel/<id> stream means events are parsed only after the response ends or the 16 MiB cap fires — normal Tlon channel messages stop reaching processEvent entirely.

Why my proof missed it

The loopback tests only exercised a server that either writes 18 MiB and closes or writes a finite set of events and closes. Both paths satisfy done. None of them prove incremental dispatch while the SSE response remains open, which is exactly the case ClawSweeper flagged.

What the right fix looks like

Bind bytes incrementally inside the existing processStream chunk loop:

  • keep a rolling byte counter
  • on each chunk, check counter + chunk.length > 16 MiB and reject with a labeled error before the chunk is fed to the SSE parser
  • preserve processEvent dispatch per \n\n frame as today
  • proof needs a loopback server that sends one event and then keeps the connection open, with the test asserting the handler fires while the response body is still reading

This is the change the reviewer asked for, and it's a much smaller, much safer patch than this one.

Lessons I'm taking forward

A bounded-read helper that materializes the entire stream before returning is the right seam for finite-body HTTP responses (OAI / Discord / OAuth) but is the wrong seam for long-lived streaming transports (SSE / WebSocket / event-source). The bounded-read campaign needs to track per-transport helper choice explicitly, not just "use the bounded reader". I'll bring that into the SKILL checklist before opening the next PR.

Closing in favor of a maintainer- or contributor-led patch that does the incremental version. Sorry for the cycle; thanks to ClawSweeper for the precise diagnosis and to @arthyn, @steipete, and @vincentkoc for the upstream SSE and bounded-read helper groundwork.

🤖 Generated with Claude Code

@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

Closed in favor of an incremental per-chunk bounded-read implementation. See comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: tlon Channel integration: tlon merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant