ANTIGRAVITY LABJP
Articles/Editor View
Editor View/2026-08-23Intermediate

A Triage Order for WebP and Opus Attachments That Never Reach the Agent

Hub 2.9.1 added WebP attachments on August 20, and CLI 1.1.17 fixed Ogg-family audio being rejected by the model on the same day. Here is why rejection happens on the sending side rather than in the file, with the MIME results I actually measured.

Antigravity354MIMEAttachmentsTroubleshooting7

I exported a fresh batch of wallpaper assets as WebP and attached them to an agent, expecting it to catch the inconsistencies in my file naming. What came back was silence. No error, no complaint — the attachment simply had not happened.

Later that week I handed over a sound effect from one of my calming apps as .opus, and the model replied that it could not handle the format. Both files open fine in any player on my machine.

Two different symptoms, but the same layer was responsible. Not the bytes in the file — the MIME type the sending side decided to stamp on it.

Hub 2.9.1, released August 20, added WebP attachments. CLI 1.1.17, released the same day, fixed .ogg, .opus, and .ogv being sent as a generic Ogg type and rejected by the model. Knowing why that happened is more useful than knowing it was fixed, because the next file that stalls will have a different extension.

Rejection happens in one of three layers

The instinct when an attachment goes missing is to re-export it in another format. That is the long way around. Decide which layer dropped it first.

LayerWhat you seeWhat to check first
1. Client and versionNothing happens. The attachment leaves no trace in the conversationYour hub / IDE / CLI version and which formats that version accepts
2. MIME resolutionThe attachment goes through, but the model says it cannot use itWhat your own machine resolves the file to (both ways, below)
3. Delivery pathThe turn gets heavy, or only a reference appearsWhether you attached it or an MCP server returned it

My two failures were layer 1 and layer 2 respectively. WebP had no intake at all on hub builds before 2.9.1. Opus had an intake, but the type stamped on it was too coarse to be useful. The Windows bug where attaching images or media files made the agent fail also sat in layer 1, and was fixed in IDE 2.5.5 on August 13.

One detail worth writing down before you move on: layer 1 and layer 2 look almost identical from the outside, and the difference is whether anything at all landed in the conversation. If the agent acknowledges a file and then declines to use it, the intake worked and the type is wrong. If the conversation stays empty, the intake never happened. That single observation decides which half of this article applies to you, and it costs nothing to make.

Layer 1 goes away when you update. Layer 2 is the awkward one, because it can come back on a different machine even after you have updated. The next section shows why.

Put the two resolution methods side by side

There are two ways to decide a file's MIME type: sniff the leading bytes, or look the extension up in a table. I ran both against the same set of files.

# Resolve from content (magic bytes)
for f in sample.png sample.webp sample.ogg sample.opus sample.ogv; do
  printf '%-13s %s\n' "$f" "$(file -b --mime-type "$f")"
done

Output on Ubuntu 22.04 with file 5.41:

sample.png    image/png
sample.webp   image/webp
sample.ogg    audio/ogg
sample.opus   audio/ogg
sample.ogv    audio/ogg

Now the extension table:

import mimetypes
 
for name in ["sample.png", "sample.webp", "sample.ogg", "sample.opus", "sample.ogv"]:
    print(f"{name:<13} {mimetypes.guess_type(name)[0]}")

Output on Python 3.10.12:

sample.png    image/png
sample.webp   None
sample.ogg    audio/ogg
sample.opus   audio/ogg
sample.ogv    video/ogg

Line them up and two separate holes appear.

FileFrom contentFrom extensionConsequence
sample.webpimage/webp(unresolved)Table-based code assigns no type and sends generic bytes
sample.opusaudio/oggaudio/oggBoth agree, but the fact that it is Opus is gone
sample.ogvaudio/oggvideo/oggAudio and video disagree with each other

All three Ogg-family files start with the same four bytes: 4f676753, which spells OggS. When audio and video share a container, no amount of byte sniffing will separate them. That is the plain reason a content-sniffing implementation lumped the whole family into one coarse type.

WebP fails from the opposite direction. There is no row for it in the extension table on this system, so no type gets assigned, and code that finds nothing usually falls back to a generic byte stream. From the model's side, nothing arrived that claimed to be an image, so declining to look at it is the correct behavior.

Opus collapses into generic Ogg because the machine's table wins

This was the part that held me up longest. Python's built-in table does know .opus as audio/opus. Run initialization, though, and the value changes underneath you.

import mimetypes
 
print(mimetypes.types_map.get(".opus"))   # before init: audio/opus
mimetypes.init()
print(mimetypes.types_map.get(".opus"))   # after init:  audio/ogg

What overwrites it is the mapping table installed on the machine. Here is the relevant line from /etc/mime.types on mine:

audio/ogg					oga ogg opus spx

opus shares a row with plain Ogg. The machine's table is read after the built-in one, so it takes precedence.

The practical consequence is that identical code produces different results on different machines. An attachment that worked on your workstation gets a different type inside a slim container or a CI runner that ships no mapping table. As an indie developer moving between a work machine and a build environment, I lost real hours to that gap — my first instinct was to blame the export settings, repeatedly.

Files arriving through MCP are typed by someone else

The third layer. Attachments are not the only way a file reaches the agent; an MCP server can hand back images or PDFs too.

On that path the type is declared by the server, not resolved by your client. Aligning your local resolution changes nothing. So the triage order has to start with a simpler question: did I attach this file, or did a tool return it?

Hub 2.9.1 changed how large binaries from connected MCP tools are handled. Instead of expanding inline, they are written to a file and referenced from the conversation. Previously an image or PDF could be expanded into the turn and break it outright. If you see a reference with no visible content, that is not a defect — that is the new storage behavior doing its job.

If the MCP server never starts in the first place, you are one step earlier than any of this. I covered that case in Find the one broken MCP entry before Antigravity starts, with 40 lines of Node.

A short check to run before you attach anything

Layer 2 can be cleared out before the file ever leaves your machine. This script compares both resolutions and surfaces disagreements and gaps.

#!/usr/bin/env python3
"""Compare content-based and extension-based MIME resolution for files you plan to attach."""
import mimetypes
import subprocess
import sys
 
# Extensions that content sniffing cannot separate, so agreement stays coarse
COARSE = {".opus": "audio/opus", ".ogv": "video/ogg", ".oga": "audio/ogg"}
 
 
def by_content(path):
    try:
        r = subprocess.run(["file", "-b", "--mime-type", path],
                           capture_output=True, text=True, check=True)
        return r.stdout.strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None
 
 
def main(paths):
    flagged = 0
    for p in paths:
        ext = "." + p.rsplit(".", 1)[-1].lower() if "." in p else ""
        by_ext = mimetypes.guess_type(p)[0]
        by_con = by_content(p)
        if by_ext is None:
            print(f"[no table entry] {p}: extension resolves to nothing (content says {by_con})")
            flagged += 1
        elif by_con and by_ext != by_con:
            print(f"[disagreement]   {p}: extension {by_ext} / content {by_con}")
            flagged += 1
        elif ext in COARSE and by_ext != COARSE[ext]:
            print(f"[too coarse]     {p}: resolves to {by_ext}, should be {COARSE[ext]}")
            flagged += 1
        else:
            print(f"[match]          {p}: {by_ext}")
    print(f"--- {flagged} to review out of {len(paths)} ---")
    return 1 if flagged else 0
 
 
if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Run against the same five files, it reports:

[match]          sample.png: image/png
[no table entry] sample.webp: extension resolves to nothing (content says image/webp)
[match]          sample.ogg: audio/ogg
[too coarse]     sample.opus: resolves to audio/ogg, should be audio/opus
[disagreement]   sample.ogv: extension video/ogg / content audio/ogg
--- 3 to review out of 5 ---

The COARSE map exists because agreement and correctness turned out to be different things. Both methods return audio/ogg for .opus. They agree perfectly, and the Opus identity is still gone. If agreement alone were the pass condition, that file would sail straight through.

One caveat on scope: this only settles what your own machine believes. It does not tell you what the client will ultimately put on the wire, and it cannot speak for a file that arrives through an MCP server. Treat a clean run as "layer 2 is not my problem right now" rather than as a guarantee, and you will use it correctly.

The non-zero exit code is there so the check can sit in front of automated steps. Anywhere assets get handed to a machine in bulk — App Store submission prep is my own case — stopping before the handoff beats debugging after it.

One thing to check next

Run the files you are about to attach through that script. The extensions that come back as [no table entry] are the ones that will lose their type on any environment without a mapping table. Those are the only ones that need a decision: either set the type explicitly on the client side, or re-export into a format with reliable coverage.

Updating is always available to you, of course. But new builds roll out gradually and can take several days to reach a given machine. Being able to name the layer while you wait turns that waiting into observation.

How to decide the shape of a multi-machine setup in the first place is something I went into in Choosing a Remote Control Host Machine Comes Down to Daemon Lifetime and Revocation Paths, alongside where credentials end up living. This piece is about the files that actually travel across that setup.

It was a small snag, but understanding it means I can now see the same shape coming. Thank you for reading.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Editor View2026-04-08
Antigravity Editor Common Errors Fix: AI Completion, Connection Issues & Project Loading Failures
Fix the most common Antigravity editor issues: AI code completion stopping, projects failing to open, terminal freezes, Git integration problems, and extension conflicts — with step-by-step solutions.
Editor View2026-08-13
2.6.0 Changed How Hooks Wait. A Triage Order for Turns That Never Finish
When an agent turn never finishes, the cause is often a hook you wrote rather than the model. Here is what changed in IDE 2.6.0, an audit script that finds hooks capable of stalling a turn, and how to give a hook its own cutoff.
Editor View2026-08-01
The Date Column Was the Most Expensive Part of My Sales CSV — Measuring What Attachments Really Cost
2.4.3 lets you attach .json, .md, and .csv files directly. I rendered the same table eight ways, priced every column in tokens, and boiled it down to a 1,062-token digest. Every number here came from a run on my own machine.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →