nesquena/hermes-webui

MEDIA: download links 404 when token is wrapped in markdown emphasis or trailing punctuation

Open

#6,890 opened on Aug 10, 2026

 (4 comments) (0 reactions) (0 assignees)Python (2,359 forks)github user discovery
bughelp wantedrenderer

Repository metrics

Stars
 (17,224 stars)
PR merge metrics
 (Avg merge 14h 31m) (314 merged PRs in 30d)

Description

Summary

Chat MEDIA:<path> download links 404 when the model wraps the token in markdown (bold/italics/code) or glues on sentence punctuation. The file on disk is fine — the frontend builds a URL that includes the trailing markup.

Repro

Agent reply containing:

Done. Here's the sheet.

**MEDIA:/data/workspace/100-camera-cctv-turnkey-comparison.xlsx**

Rendered link label becomes 📎 100-camera-cctv-turnkey-comparison.xlsx** and the href is:

api/media?path=%2Fdata%2Fworkspace%2F100-camera-cctv-turnkey-comparison.xlsx%2A%2A&download=1

GET /api/media with the clean path → 200 + xlsx bytes. Same request with the trailing **404 {"error":"not found"}.

Root cause

In static/ui.js renderMd() (and the matching streaming path in static/messages.js), MEDIA tokens are stashed before markdown runs:

s=s.replace(/MEDIA:([^\s\)\]]+)/g,(_,raw_ref)=>{
  media_stash.push(raw_ref);
  return '\x00D'+(media_stash.length-1)+'\x00';
});

[^\s\)\]]+ does not stop at *, `, _, or sentence punctuation, so closing ** / backticks / a trailing . are captured into the path. _inlineMediaHtmlForRef then encodeURIComponents that polluted ref into /api/media.

The plain-URL autolink pass already strips trailing [.,;:!?)] — MEDIA does not.

Scale

On one production device: 106 / 317 MEDIA tokens (~33%) had trailing junk captured this way (mostly trailing ., then backticks, then **).

Proposed fix

Mirror what the Hermes iOS client already does in TranscriptMediaParser (see nesquena/hermes-webui consumers / Performance Hub iOS TranscriptMedia.swift):

  1. After capturing the ref (or inside _inlineMediaHtmlForRef), strip trailing sentence punctuation [.,;:!?]+.
  2. If the token is immediately preceded by a markdown emphasis/code delimiter (***/___/**/__/*/_/`), also strip the matching closing delimiter from the end of the ref so it stays outside the path.
  3. Apply the same normalisation in the streaming messages.js MEDIA path (it shares _inlineMediaHtmlForRef today, so fixing that helper once covers both settled + live rendering + historical transcripts on re-paint).

Happy to send a PR if useful — smallest change is a _normalizeMediaRef(ref) helper called from _inlineMediaHtmlForRef (and optionally from the stash callback so leftover opening ** can still bold-wrap the link cleanly).

Environment

Observed on hermes-webui 0.52.151 (ghcr.io/nesquena/hermes-webui@sha256:75efbb5f…); master static/ui.js still has the same stash regex as of this report.

Contributor guide