opencode
opencode copied to clipboard
fix(acp): preserve file attachment metadata during session replay
Problem
When loading a previous session via ACP, file attachments are not replayed correctly:
- Text files (JSON, plain text, etc.) are not sent as resource blocks
- Binary files (PDFs, audio, video) are not sent as resource blocks
- Synthetic text parts leak to ACP clients during replay
- Non-binary file parts cause LLM provider errors when passed through
- Audio content in prompts is not handled
Root Cause
In processMessage(), there was no handling for file type parts during session replay - they were silently skipped. Additionally:
- Text parts were sent without checking
part.synthetic - In
toModelMessage(), all file parts were sent to the LLM regardless of MIME type - No handling for ACP
audiocontent type in prompts
Solution
Option 1: Blacklist text MIME types
List specific text types and handle them differently.
Problem: Fragile - new types would break, and the list keeps growing.
Option 2: Whitelist binary types (chosen)
Categorize content by what LLMs and ACP clients actually support.
Benefits:
- Robust: Unknown MIME types have safe fallback behavior
- Provider-agnostic: Matches what LLM APIs actually accept
- Simple: Clear categories instead of unbounded lists
Changes
1. Replay file parts as ACP content blocks (agent.ts - processMessage)
- Images: Send as
imageblock with base64 data - Binary files (PDF, audio, video): Send as
resourceblock withblobfield - Text files: Decode and send as
resourceblock withtextfield
2. Add audio content support (agent.ts - prompt)
Handle ACP audio content type (per ACP spec):
case "audio":
if (part.data) {
parts.push({
type: "file",
url: `data:${part.mimeType};base64,${part.data}`,
filename: `audio.${ext}`,
mime: part.mimeType,
})
}
break
3. Filter synthetic parts during replay (agent.ts)
if (part.text && !part.synthetic) {
// Send to ACP
}
4. Whitelist binary content for LLM (message-v2.ts)
const isBinaryContent =
part.mime.startsWith("image/") ||
part.mime.startsWith("audio/") ||
part.mime.startsWith("video/") ||
part.mime === "application/pdf"
if (isBinaryContent) {
// Keep as file part for LLM
} else {
// Decode to text with filename header
}
Summary
| Content Type | ACP Replay | LLM Input |
|---|---|---|
| image/* | image block (data) | file part |
| audio/* | resource block (blob) | file part |
| video/* | resource block (blob) | file part |
| application/pdf | resource block (blob) | file part |
| text/*, application/json, etc. | resource block (text) | decoded to text |
| Unknown | resource block (text) | decoded to text |