fix(attachments): address PR review feedback (Copilot + code-quality)

- TextDecoder in the text-preview stream now flushes on completion/cancel, so a multibyte UTF-8 char split across a chunk boundary isn't dropped (Copilot).

- send() clears self._wire_part_cache in a finally so the per-send memo (which can hold large rasterized PDF page-images) is released at send end instead of retained on an idle session until the next send (Copilot + fix-review).

- Make the implicit byte-string concatenation in _minimal_pdf explicit (+) in test_pdf.py and test_thumbnails.py so it can't read as a missing comma (CodeQL / github-code-quality).
This commit is contained in:
Patrick Buckley
2026-06-15 21:26:01 -07:00
parent d81b312b41
commit c099ed030e
4 changed files with 12 additions and 5 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ def _minimal_pdf(text: str = "Hello PDF") -> bytes:
b"<</Type/Catalog/Pages 2 0 R>>",
b"<</Type/Pages/Kids[3 0 R]/Count 1>>",
b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 300 144]"
b"/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>",
+ b"/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>",
b"<</Length %d>>\nstream\n%s\nendstream" % (len(stream), stream),
b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
]
+1 -1
View File
@@ -20,7 +20,7 @@ def _minimal_pdf(text: str = "Hi") -> bytes:
b"<</Type/Catalog/Pages 2 0 R>>",
b"<</Type/Pages/Kids[3 0 R]/Count 1>>",
b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 300 144]"
b"/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>",
+ b"/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>",
b"<</Length %d>>\nstream\n%s\nendstream" % (len(stream), stream),
b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
]
+9 -3
View File
@@ -2969,9 +2969,9 @@ class ChatSession:
# re-rasterized / a blob re-base64'd once per round-trip. Key on
# (id, caps-signature): the same stored blob materializes differently per
# capability set, and a fallback to a different-caps model can resolve
# within one send. The cache is refreshed per send (set in send()); the
# wire resolver runs only during a send, so a stale value between sends is
# never read. A None cache disables memoization (the original behavior).
# within one send. Set in send() and cleared in its finally, so it is
# None outside a send; the wire resolver runs only during a send. A None
# cache disables memoization (the original behavior).
cache = self._wire_part_cache
caps_sig = (caps.supports_pdf, caps.supports_vision, caps.supports_audio_input)
out: dict[str, Any] = {}
@@ -4600,6 +4600,12 @@ class ChatSession:
self._drain_pending_advisories()
self._record_fatal_error(exc)
raise
finally:
# Release the per-send wire-part memo (it can hold large rasterized
# PDF page-images) so it is GC'd at send end rather than retained on
# an idle session until the next send. Restores the "None outside a
# send" invariant on every exit (success, cancel, or error).
self._wire_part_cache = None
def _drain_pending_advisories(self) -> None:
"""Drop every pending nudge regardless of channel.
@@ -139,6 +139,7 @@ export function buildAttachmentPreview(opts) {
if (res && res.value)
acc += dec.decode(res.value, { stream: true });
if (res.done || acc.length >= 240) {
acc += dec.decode(); // flush bytes buffered across a chunk boundary
try {
reader.cancel();
} catch (e) {