6fad6f1dd8
Port from nanocoai/nanoclaw#2895's never-silently-drop guarantee. Before: saveMedia() in scripts/whatsapp-bridge/bridge_helpers.js awaited downloadMedia() with no try/catch. A failed CDN fetch (expired media URL, transient network error — Baileys throws 'Failed to fetch stream from https://mmg.whatsapp.net/...') rejected out of extractBridgeEvent, which bridge.js awaits inside its messages.upsert for-loop with no per-message guard — dropping the failed message AND every remaining message in the same upsert batch, silently. After: - saveMedia catches download/write failures, records the media type, and logs a console.warn instead of rejecting. - appendMediaFailureNote() (exported pure helper, mirroring the file's testable-helper convention) surfaces '[<type> could not be downloaded]' in the event body, so the agent learns media was sent rather than the attachment vanishing. Applied before the '[<type> received]' fallback so an uncaptioned failed image reads as a failure, not an arrival. The reuploadRequest recovery half of nanoclaw#2895 is already wired in bridge.js (downloadMediaMessage(..., { reuploadRequest: sock.updateMediaMessage })); this ports the containment half hermes was missing. Tests: 3 new cases in bridge.native.test.mjs (note formatting, uncaptioned failure containment, captioned failure note). All 5 bridge test files pass.