Skip to content

feat(harness): file attachment support with filename preservation and text file handling#13574

Merged
abhiaiyer91 merged 4 commits into
mastra-ai:mainfrom
superset-sh:superset/harness-file-attachments
Feb 28, 2026
Merged

feat(harness): file attachment support with filename preservation and text file handling#13574
abhiaiyer91 merged 4 commits into
mastra-ai:mainfrom
superset-sh:superset/harness-file-attachments

Conversation

@saddlepaddle
Copy link
Copy Markdown
Contributor

@saddlepaddle saddlepaddle commented Feb 27, 2026

Summary

  • Rename imagesfiles in sendMessage API to support all file types, not just images
  • Use AI SDK FilePart shape (mimeType instead of mediaType) for model compatibility
  • Preserve filename field through AIV4Adapter and AIV5Adapter when storing file parts to DB — previously filename was silently dropped during serialization, so it was present during streaming but lost in message history
  • Handle text-based files (text/*, application/json) by decoding them to text content parts instead of sending as binary file parts, which models like Claude cannot process inline

Test plan

  • Send an image attachment → model responds correctly, filename persists in message history
  • Send a .md or .json file → model receives file content as text and responds (previously hung)
  • Verify filename shows in message history after page reload (not just during streaming)

Summary by CodeRabbit

  • New Features

    • File attachment support in messages with optional filename preservation and improved media-type handling.
    • Text-based files (e.g., text/*, application/json) now decode into readable text content when appropriate.
  • Changes

    • Message send API parameter renamed from images to files; files include mediaType and optional filename (breaking change).
    • File metadata is preserved across message conversions, storage, and message history.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 27, 2026

🦋 Changeset detected

Latest commit: 434fb31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@mastra/core Patch
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/react Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/playground-ui Patch
@mastra/server Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch
@mastra/deployer-vercel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 27, 2026

@saddlepaddle is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces harness images parameter with files ({ data, mediaType, filename? }), adds a file message content variant, preserves filename through AIV4/AIV5 adapter conversions, and updates the TUI to map outbound images into files with mediaType when calling sendMessage.

Changes

Cohort / File(s) Summary
Harness Public API & Types
packages/core/src/harness/harness.ts, packages/core/src/harness/types.ts
Renamed sendMessage parameter from images to files (mimeTypemediaType, optional filename). Added file variant to HarnessMessageContent. Mapped input files to message fileParts and supported file parts in streaming and normalization paths.
AI Adapter Filename Propagation (AIV4)
packages/core/src/agent/message-list/adapters/AIV4Adapter.ts
Propagates aiV4Part.filename into resulting DB file parts across multiple branches when converting AI V4 content, ensuring filename is preserved on generated parts.
AI Adapter Filename Propagation (AIV5)
packages/core/src/agent/message-list/adapters/AIV5Adapter.ts
Adds optional filename propagation in both directions: toUIMessage includes filename from V2 file parts, and fromUIMessage sets V2 file part filename from UI parts.
TUI Message Transmission
mastracode/src/tui/mastra-tui.ts
Changed outbound mapping to convert images into file objects with { data, mediaType } and pass them as files to sendMessage (TUI does not attach filenames).
Changelog / Notes
.changeset/file-attachment-support.md
Documents file-attachment support, breaking API rename (imagesfiles), optional filename preservation, and text-file decoding behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title exceeds the recommended 50-character limit (88 characters) and is overly detailed, violating the conciseness requirement. Shorten to under 50 characters by removing redundant details: e.g., 'feat(harness): add file attachment support' or 'feat(harness): file attachment support'.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@saddlepaddle saddlepaddle force-pushed the superset/harness-file-attachments branch from 698ec6d to 67e0323 Compare February 27, 2026 07:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/agent/message-list/adapters/AIV5Adapter.ts (2)

425-433: ⚠️ Potential issue | 🟠 Major

Avoid cast-based filename handling; keep it strictly typed.

The (p as { filename?: string }) and (v2FilePart as Record<string, unknown>) pattern bypasses strict typing and makes filename propagation fragile. Also, truthy checks can unintentionally drop empty-string filenames.

💡 Proposed fix
@@
-        if (p.type === 'file') {
+        if (p.type === 'file') {
+          const filename = 'filename' in p ? (p as { filename?: string }).filename : undefined;
           return {
             type: 'file' as const,
             mimeType: p.mediaType,
             data: p.url || '',
             providerMetadata: p.providerMetadata,
-            ...((p as { filename?: string }).filename ? { filename: (p as { filename?: string }).filename } : {}),
+            ...(filename !== undefined ? { filename } : {}),
           };
         }
@@
-        const v2FilePart: MastraDBMessage['content']['parts'][number] = {
+        const v2FilePart: MastraDBMessage['content']['parts'][number] & { filename?: string } = {
           type: 'file',
           data: fileData,
           mimeType,
         };
@@
-        if ((filePart as { filename?: string }).filename) {
-          (v2FilePart as Record<string, unknown>).filename = (filePart as { filename?: string }).filename;
+        if ('filename' in filePart && filePart.filename !== undefined) {
+          v2FilePart.filename = filePart.filename;
         }

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript with strict type checking for all packages in the monorepo.

Also applies to: 667-677

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/message-list/adapters/AIV5Adapter.ts` around lines
425 - 433, The file-handling branch in AIV5Adapter.ts uses unsafe casts ((p as {
filename?: string }) and elsewhere) and a truthy check that drops empty-string
filenames; replace these with a proper type-narrowing guard and explicit
filename presence check: ensure the file-part union/interface used by the
mapping (the object inspected when p.type === 'file') declares filename?:
string, then test e.g. 'filename' in p && typeof p.filename === "string" to
include filename even when empty, and remove the ad-hoc casts ((p as {...}) and
(v2FilePart as Record<string, unknown>)); apply the same change to the other
similar block around the later file handling (lines ~667-677) so filename
propagation is strictly typed and not lost for empty strings.

196-218: ⚠️ Potential issue | 🟠 Major

filename is not round-tripped back to UI file parts.

fromUIMessage/fromModelMessage can store filename, but toUIMessage drops it in both file conversion branches. This breaks the PR goal of filename persistence after reload/history reconstruction.

💡 Proposed fix
@@
-          if (categorized.type === 'url' && typeof part.data === 'string') {
+          if (categorized.type === 'url' && typeof part.data === 'string') {
+            const filename = 'filename' in part ? (part as { filename?: string }).filename : undefined;
             const v5UIPart: AIV5Type.FileUIPart = {
               type: 'file' as const,
               url: part.data,
               mediaType: categorized.mimeType || 'image/png',
+              ...(filename !== undefined ? { filename } : {}),
             };
@@
-            const v5UIPart: AIV5Type.FileUIPart = {
+            const filename = 'filename' in part ? (part as { filename?: string }).filename : undefined;
+            const v5UIPart: AIV5Type.FileUIPart = {
               type: 'file' as const,
               url: dataUri,
               mediaType: finalMimeType,
+              ...(filename !== undefined ? { filename } : {}),
             };

Also applies to: 246-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/message-list/adapters/AIV5Adapter.ts` around lines
196 - 218, The UI file-part conversion in AIV5Adapter (inside the file-to-UI
conversion that uses categorizeFileData and constructs AIV5Type.FileUIPart
objects) drops the original filename; preserve and propagate part.filename into
the constructed v5UIPart in both branches (the 'url' branch that builds
{type:'file', url,...} and the alternate/raw branch that creates a file UI
part), and also keep existing providerMetadata; update any other conversion
branch that handles file parts (the second block noted in the diff) to copy
part.filename into the resulting UI file part so filenames round-trip through
toUIMessage.
🧹 Nitpick comments (1)
packages/core/src/harness/harness.ts (1)

1327-1332: Preserve filename when normalizing image parts to file.

The normalization branch keeps data/media type but drops filename metadata if it exists on the source part.

♻️ Proposed refactor
           content.push({
             type: 'file',
             data: imgData,
             mediaType:
               (part as { mimeType?: string }).mimeType ?? (part as { mediaType?: string }).mediaType ?? 'image/png',
+            ...((part as { filename?: string }).filename ? { filename: (part as { filename?: string }).filename } : {}),
           });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1327 - 1332, The
normalization that converts image parts into file parts (the content.push block
that creates objects with type: 'file', data: imgData, mediaType: ...) currently
drops any filename metadata from the original part; update that creation to copy
the filename through (e.g. add filename: (part as { filename?: string
}).filename ?? undefined) so the resulting file object preserves the source
filename when present (ensure you reference the same `part` and `imgData`
variables used in the snippet).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/agent/message-list/adapters/AIV4Adapter.ts`:
- Around line 432-434: The stored file `filename` is not propagated when
converting stored `file` parts into UI `experimental_attachments` in the
AIV4Adapter.toUIMessage conversion, causing filenames to be lost on hydration;
update the code that constructs `experimental_attachments` (within toUIMessage)
to copy the `filename` from the stored part (`aiV4Part` / `part`) into the
attachment object (add a filename field on the built experimental_attachments
entry) for all file-part construction sites (the blocks around lines 88-109 and
the other similar blocks at the ranges you noted) so the round-trip preserves
filename metadata.

In `@packages/core/src/harness/harness.ts`:
- Around line 1099-1107: The current logic only handles ;base64 data URIs and
leaves plain data:text/... URIs as raw strings; update the parsing for f.data so
it matches a full data URI (e.g.
/^data:([^;]+)(?:;charset=[^;]+)?(?:;base64)?,(.*)$/i) and capture the payload;
if the captured group indicates ;base64 then decode with
Buffer.from(...,'base64'), otherwise URL-decode the payload (decodeURIComponent,
handling plus-to-space if needed) and set textContent accordingly; adjust the
existing base64Match usage and textContent assignment in the same block to use
this new match and branching logic.

---

Outside diff comments:
In `@packages/core/src/agent/message-list/adapters/AIV5Adapter.ts`:
- Around line 425-433: The file-handling branch in AIV5Adapter.ts uses unsafe
casts ((p as { filename?: string }) and elsewhere) and a truthy check that drops
empty-string filenames; replace these with a proper type-narrowing guard and
explicit filename presence check: ensure the file-part union/interface used by
the mapping (the object inspected when p.type === 'file') declares filename?:
string, then test e.g. 'filename' in p && typeof p.filename === "string" to
include filename even when empty, and remove the ad-hoc casts ((p as {...}) and
(v2FilePart as Record<string, unknown>)); apply the same change to the other
similar block around the later file handling (lines ~667-677) so filename
propagation is strictly typed and not lost for empty strings.
- Around line 196-218: The UI file-part conversion in AIV5Adapter (inside the
file-to-UI conversion that uses categorizeFileData and constructs
AIV5Type.FileUIPart objects) drops the original filename; preserve and propagate
part.filename into the constructed v5UIPart in both branches (the 'url' branch
that builds {type:'file', url,...} and the alternate/raw branch that creates a
file UI part), and also keep existing providerMetadata; update any other
conversion branch that handles file parts (the second block noted in the diff)
to copy part.filename into the resulting UI file part so filenames round-trip
through toUIMessage.

---

Nitpick comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 1327-1332: The normalization that converts image parts into file
parts (the content.push block that creates objects with type: 'file', data:
imgData, mediaType: ...) currently drops any filename metadata from the original
part; update that creation to copy the filename through (e.g. add filename:
(part as { filename?: string }).filename ?? undefined) so the resulting file
object preserves the source filename when present (ensure you reference the same
`part` and `imgData` variables used in the snippet).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 257d14f and 698ec6d.

📒 Files selected for processing (9)
  • mastracode/src/__tests__/create-auth-storage.test.ts
  • mastracode/src/agents/__tests__/tools.test.ts
  • mastracode/src/agents/tools.ts
  • mastracode/src/index.ts
  • mastracode/src/tui/mastra-tui.ts
  • packages/core/src/agent/message-list/adapters/AIV4Adapter.ts
  • packages/core/src/agent/message-list/adapters/AIV5Adapter.ts
  • packages/core/src/harness/harness.ts
  • packages/core/src/harness/types.ts

Comment thread packages/core/src/agent/message-list/adapters/AIV4Adapter.ts
Comment on lines +1099 to +1107
// Decode data URI to plain text
const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/);
if (base64Match) {
try {
textContent = Buffer.from(base64Match[1]!, 'base64').toString('utf-8');
} catch {
// Fall through with raw data
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Decode non-base64 data URIs for text attachments.

Line 1100 only decodes ;base64 URIs. For data:text/plain,... payloads, the raw URI string is forwarded as content instead of decoded text.

💡 Proposed fix
-            // Decode data URI to plain text
-            const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/);
-            if (base64Match) {
-              try {
-                textContent = Buffer.from(base64Match[1]!, 'base64').toString('utf-8');
-              } catch {
-                // Fall through with raw data
-              }
-            }
+            // Decode data URI to plain text (supports base64 and url-encoded forms)
+            const dataUriMatch = f.data.match(/^data:([^,]*),(.*)$/s);
+            if (dataUriMatch) {
+              const meta = dataUriMatch[1] ?? '';
+              const payload = dataUriMatch[2] ?? '';
+              try {
+                textContent = /;base64/i.test(meta)
+                  ? Buffer.from(payload, 'base64').toString('utf-8')
+                  : decodeURIComponent(payload);
+              } catch {
+                // Fall through with raw data
+              }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Decode data URI to plain text
const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/);
if (base64Match) {
try {
textContent = Buffer.from(base64Match[1]!, 'base64').toString('utf-8');
} catch {
// Fall through with raw data
}
}
// Decode data URI to plain text (supports base64 and url-encoded forms)
const dataUriMatch = f.data.match(/^data:([^,]*),(.*)$/s);
if (dataUriMatch) {
const meta = dataUriMatch[1] ?? '';
const payload = dataUriMatch[2] ?? '';
try {
textContent = /;base64/i.test(meta)
? Buffer.from(payload, 'base64').toString('utf-8')
: decodeURIComponent(payload);
} catch {
// Fall through with raw data
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1099 - 1107, The current
logic only handles ;base64 data URIs and leaves plain data:text/... URIs as raw
strings; update the parsing for f.data so it matches a full data URI (e.g.
/^data:([^;]+)(?:;charset=[^;]+)?(?:;base64)?,(.*)$/i) and capture the payload;
if the captured group indicates ;base64 then decode with
Buffer.from(...,'base64'), otherwise URL-decode the payload (decodeURIComponent,
handling plus-to-space if needed) and set textContent accordingly; adjust the
existing base64Match usage and textContent assignment in the same block to use
this new match and branching logic.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/core/src/harness/harness.ts (1)

1099-1107: ⚠️ Potential issue | 🟡 Minor

Decode non-base64 text data URIs before building text parts.

Line 1100 currently decodes only ;base64 payloads. data:text/plain,... and similar URL-encoded forms are passed through undecoded.

Suggested fix
-            // Decode data URI to plain text
-            const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/);
-            if (base64Match) {
-              try {
-                textContent = Buffer.from(base64Match[1]!, 'base64').toString('utf-8');
-              } catch {
-                // Fall through with raw data
-              }
-            }
+            // Decode data URI to plain text (base64 + url-encoded forms)
+            const dataUriMatch = f.data.match(/^data:([^,]*),(.*)$/s);
+            if (dataUriMatch) {
+              const meta = dataUriMatch[1] ?? '';
+              const payload = dataUriMatch[2] ?? '';
+              try {
+                textContent = /;base64/i.test(meta)
+                  ? Buffer.from(payload, 'base64').toString('utf-8')
+                  : decodeURIComponent(payload.replace(/\+/g, '%20'));
+              } catch {
+                // Fall through with raw data
+              }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1099 - 1107, The current
block only decodes data URIs with ";base64" and leaves URL-encoded payloads like
"data:text/plain,Hello%20World" undecoded; update the logic around f.data and
textContent to handle non-base64 data URIs too by matching a non-base64 pattern
(e.g. /^data:[^,]*,(.*)$/), and when base64 is not present call
decodeURIComponent on the captured payload (with a safe try/catch) to set
textContent; keep the existing Buffer.from(...,'base64') branch for the ;base64
case and fall back to the raw data only if both decoding attempts fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 1099-1107: The current block only decodes data URIs with ";base64"
and leaves URL-encoded payloads like "data:text/plain,Hello%20World" undecoded;
update the logic around f.data and textContent to handle non-base64 data URIs
too by matching a non-base64 pattern (e.g. /^data:[^,]*,(.*)$/), and when base64
is not present call decodeURIComponent on the captured payload (with a safe
try/catch) to set textContent; keep the existing Buffer.from(...,'base64')
branch for the ;base64 case and fall back to the raw data only if both decoding
attempts fail.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 698ec6d and 67e0323.

📒 Files selected for processing (5)
  • mastracode/src/tui/mastra-tui.ts
  • packages/core/src/agent/message-list/adapters/AIV4Adapter.ts
  • packages/core/src/agent/message-list/adapters/AIV5Adapter.ts
  • packages/core/src/harness/harness.ts
  • packages/core/src/harness/types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/core/src/agent/message-list/adapters/AIV5Adapter.ts
  • packages/core/src/harness/types.ts
  • packages/core/src/agent/message-list/adapters/AIV4Adapter.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/file-attachment-support.md:
- Around line 6-11: Add a brief public API example and migration note showing
how callers should change sendMessage(images: ...) to sendMessage(files: ...),
include a before/after snippet that demonstrates preserving filename and text
decoding (e.g., passing {filename, mimeType, data} in files) and mention that
HarnessMessageContent now accepts type "file" so convertToHarnessMessage will
round-trip file parts; reference sendMessage, images -> files,
HarnessMessageContent, convertToHarnessMessage, AIV4Adapter, and AIV5Adapter so
readers know the adapters preserve filename and text-based files are decoded to
text parts rather than binary.
- Around line 8-11: Update the changeset bullets to be user-facing: replace
internal references (AIV4Adapter, AIV5Adapter, HarnessMessageContent,
convertToHarnessMessage) with outcome-focused language describing what users can
do and what changed — e.g., mention that sendMessage now accepts files (renamed
from images) of any type and preserves original filenames, text/* and
application/json files are stored/handled as readable text rather than binary,
and the new file message type enables proper round-tripping — keep references to
internal symbols only for maintainers if necessary but primarily state the
visible behaviors and benefits to users.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67e0323 and 9e37fce.

📒 Files selected for processing (1)
  • .changeset/file-attachment-support.md

Comment thread .changeset/file-attachment-support.md Outdated
Comment thread .changeset/file-attachment-support.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.changeset/file-attachment-support.md (1)

26-26: 🛠️ Refactor suggestion | 🟠 Major

Make the bullet more user-facing; avoid focusing on internal type names.

Line 26 mentions HarnessMessageContent (an internal type) rather than emphasizing the user outcome. Reword to focus on what developers experience.

♻️ Suggested rewording
-- `HarnessMessageContent` now includes a `file` type, so file parts round-trip correctly through message history
+- File attachments are preserved correctly in message history and remain accessible after page reload

As per coding guidelines: "Highlight outcomes! What does change for the end user? Do not focus on internal implementation details."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/file-attachment-support.md at line 26, Summary: The bullet
references the internal type HarnessMessageContent and should be reworded to
focus on the user-facing outcome. Replace the technical mention of
HarnessMessageContent and the `file` type with a developer-facing outcome
sentence such as: "Message parts can now include file attachments, so files are
preserved and round-trip correctly through message history." Update the bullet
text to this user-focused phrasing and remove or avoid mentioning the internal
type name HarnessMessageContent or the literal `file` type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.changeset/file-attachment-support.md:
- Line 26: Summary: The bullet references the internal type
HarnessMessageContent and should be reworded to focus on the user-facing
outcome. Replace the technical mention of HarnessMessageContent and the `file`
type with a developer-facing outcome sentence such as: "Message parts can now
include file attachments, so files are preserved and round-trip correctly
through message history." Update the bullet text to this user-focused phrasing
and remove or avoid mentioning the internal type name HarnessMessageContent or
the literal `file` type.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e37fce and e17174e.

📒 Files selected for processing (1)
  • .changeset/file-attachment-support.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/core/src/harness/harness.ts (1)

1099-1107: ⚠️ Potential issue | 🟡 Minor

Handle non-base64 data: URIs for text attachments.

Line 1100 only parses ;base64 payloads. data:text/plain,... or other URL-encoded data: payloads will be forwarded as raw URI text instead of decoded content.

💡 Suggested fix
-            // Decode data URI to plain text
-            const base64Match = f.data.match(/^data:[^;]*;base64,(.*)$/);
-            if (base64Match) {
-              try {
-                textContent = Buffer.from(base64Match[1]!, 'base64').toString('utf-8');
-              } catch {
-                // Fall through with raw data
-              }
-            }
+            // Decode data URI to plain text (base64 + URL-encoded forms)
+            const dataUriMatch = f.data.match(/^data:([^,]*),(.*)$/s);
+            if (dataUriMatch) {
+              const meta = dataUriMatch[1] ?? '';
+              const payload = dataUriMatch[2] ?? '';
+              try {
+                textContent = /;base64/i.test(meta)
+                  ? Buffer.from(payload, 'base64').toString('utf-8')
+                  : decodeURIComponent(payload.replace(/\+/g, '%20'));
+              } catch {
+                // Fall through with raw data
+              }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1099 - 1107, The current
data URI handling only decodes base64 payloads (using
f.data.match(/^data:[^;]*;base64,(.*)$/)), so non-base64 data URIs like
data:text/plain,Hello%20World are left as raw URIs; update the parsing in the
same block that sets textContent to also match general data URIs (e.g.,
/^data:([^,]*),(.*)$/), and when the media-type part does not include ";base64"
decode the payload part with decodeURIComponent (after replacing '+' with ' ')
to produce the plain text; keep the existing base64 branch
(Buffer.from(...,'base64').toString('utf-8')) and fall back to the original raw
f.data only if decoding fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 1327-1333: The normalization drops the source part's filename when
converting an image to a file: in the branch that calls content.push({ type:
'file', data: imgData, mediaType: ... }) ensure the original part.filename (if
present) is preserved by copying it into the resulting file object (either as
data.filename or a top-level filename property on the pushed object). Update the
content.push call that constructs the file entry (and the imgData object if
that's where filenames belong) to set filename = (part as any).filename when
available, keeping existing mediaType handling intact.

---

Duplicate comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 1099-1107: The current data URI handling only decodes base64
payloads (using f.data.match(/^data:[^;]*;base64,(.*)$/)), so non-base64 data
URIs like data:text/plain,Hello%20World are left as raw URIs; update the parsing
in the same block that sets textContent to also match general data URIs (e.g.,
/^data:([^,]*),(.*)$/), and when the media-type part does not include ";base64"
decode the payload part with decodeURIComponent (after replacing '+' with ' ')
to produce the plain text; keep the existing base64 branch
(Buffer.from(...,'base64').toString('utf-8')) and fall back to the original raw
f.data only if decoding fails.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e17174e and 53f2c85.

📒 Files selected for processing (3)
  • mastracode/src/tui/mastra-tui.ts
  • packages/core/src/harness/harness.ts
  • packages/core/src/harness/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/harness/types.ts
  • mastracode/src/tui/mastra-tui.ts

Comment on lines +1327 to +1333
content.push({
type: 'file',
data: imgData,
mediaType:
(part as { mimeType?: string }).mimeType ?? (part as { mediaType?: string }).mediaType ?? 'image/png',
});
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve filename when normalizing image parts to file.

This branch converts image to file but drops filename if it exists on the source part, which can still lose metadata in history normalization paths.

💡 Suggested fix
           content.push({
             type: 'file',
             data: imgData,
             mediaType:
               (part as { mimeType?: string }).mimeType ?? (part as { mediaType?: string }).mediaType ?? 'image/png',
+            ...((part as { filename?: string }).filename ? { filename: (part as { filename?: string }).filename } : {}),
           });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1327 - 1333, The
normalization drops the source part's filename when converting an image to a
file: in the branch that calls content.push({ type: 'file', data: imgData,
mediaType: ... }) ensure the original part.filename (if present) is preserved by
copying it into the resulting file object (either as data.filename or a
top-level filename property on the pushed object). Update the content.push call
that constructs the file entry (and the imgData object if that's where filenames
belong) to set filename = (part as any).filename when available, keeping
existing mediaType handling intact.

… text file handling

- Rename images→files in sendMessage API to support all file types
- Use AI SDK FilePart shape (mimeType instead of mediaType) for model compatibility
- Preserve filename field through AIV4Adapter and AIV5Adapter when storing file parts to DB
- Decode text-based files (text/*, application/json) to text content parts instead of sending
  as binary file parts, which models cannot process
@saddlepaddle saddlepaddle force-pushed the superset/harness-file-attachments branch from 53f2c85 to 81274b8 Compare February 28, 2026 04:09
@abhiaiyer91 abhiaiyer91 merged commit 276246e into mastra-ai:main Feb 28, 2026
11 of 20 checks passed
abhiaiyer91 added a commit that referenced this pull request Mar 3, 2026
…s step scoring guard

- message-list tests: add expected `filename` property after #13574
  introduced filename preservation in AIV4Adapter.fromCoreMessage()
- evals/run: fix guard that required `stepResult.payload`, which the
  workflow engine strips when it matches previous output. Use
  `scoringData.input` as fallback for scorer input.
- Remove stale debug console.log from evals test

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-6) <noreply@mastra.ai>
saddlepaddle added a commit to superset-sh/mastra that referenced this pull request Mar 5, 2026
…arts

The file part object passed `mimeType` instead of `mediaType`, causing
file attachments to be routed through the V4 adapter instead of V5.

Introduced in mastra-ai#13574.
abhiaiyer91 pushed a commit that referenced this pull request Mar 5, 2026
…arts (#13833)

## Summary

Fixes a typo in `Harness.sendMessage()` where file parts were
constructed with `mimeType` instead of `mediaType`. This caused file
attachments to be routed through the V4 adapter instead of V5,
preventing them from being correctly processed by AI SDK v5 providers.

## Change

**`packages/core/src/harness/harness.ts`**

```diff
- return { type: 'file' as const, data: f.data, mimeType: f.mediaType, filename: f.filename };
+ return { type: 'file' as const, data: f.data, mediaType: f.mediaType, filename: f.filename };
```

## Context

The `files` parameter was added in #13574, but the file part
construction used `mimeType` (V4 format) instead of `mediaType` (V5
format). The AI SDK v5 `FilePart` type expects `mediaType`, so using
`mimeType` causes the part to not match the expected shape and fall
through to the legacy adapter path.

This bug is present in `@mastra/core@1.9.0` on npm.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Corrected file attachment metadata naming in the message system to
ensure files are properly routed and processed by supported AI SDK
providers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
graysonhicks pushed a commit that referenced this pull request Mar 5, 2026
…arts (#13833)

## Summary

Fixes a typo in `Harness.sendMessage()` where file parts were
constructed with `mimeType` instead of `mediaType`. This caused file
attachments to be routed through the V4 adapter instead of V5,
preventing them from being correctly processed by AI SDK v5 providers.

## Change

**`packages/core/src/harness/harness.ts`**

```diff
- return { type: 'file' as const, data: f.data, mimeType: f.mediaType, filename: f.filename };
+ return { type: 'file' as const, data: f.data, mediaType: f.mediaType, filename: f.filename };
```

## Context

The `files` parameter was added in #13574, but the file part
construction used `mimeType` (V4 format) instead of `mediaType` (V5
format). The AI SDK v5 `FilePart` type expects `mediaType`, so using
`mimeType` causes the part to not match the expected shape and fall
through to the legacy adapter path.

This bug is present in `@mastra/core@1.9.0` on npm.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Corrected file attachment metadata naming in the message system to
ensure files are properly routed and processed by supported AI SDK
providers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
wardpeet pushed a commit that referenced this pull request Mar 9, 2026
… text file handling (#13574)

## Summary

- **Rename `images` → `files`** in `sendMessage` API to support all file
types, not just images
- **Use AI SDK `FilePart` shape** (`mimeType` instead of `mediaType`)
for model compatibility
- **Preserve `filename` field** through `AIV4Adapter` and `AIV5Adapter`
when storing file parts to DB — previously `filename` was silently
dropped during serialization, so it was present during streaming but
lost in message history
- **Handle text-based files** (`text/*`, `application/json`) by decoding
them to text content parts instead of sending as binary `file` parts,
which models like Claude cannot process inline

## Test plan

- [ ] Send an image attachment → model responds correctly, filename
persists in message history
- [ ] Send a `.md` or `.json` file → model receives file content as text
and responds (previously hung)
- [ ] Verify filename shows in message history after page reload (not
just during streaming)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* File attachment support in messages with optional filename
preservation and improved media-type handling.
* Text-based files (e.g., text/*, application/json) now decode into
readable text content when appropriate.

* **Changes**
* Message send API parameter renamed from images to files; files include
mediaType and optional filename (breaking change).
* File metadata is preserved across message conversions, storage, and
message history.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
wardpeet pushed a commit that referenced this pull request Mar 9, 2026
…arts (#13833)

## Summary

Fixes a typo in `Harness.sendMessage()` where file parts were
constructed with `mimeType` instead of `mediaType`. This caused file
attachments to be routed through the V4 adapter instead of V5,
preventing them from being correctly processed by AI SDK v5 providers.

## Change

**`packages/core/src/harness/harness.ts`**

```diff
- return { type: 'file' as const, data: f.data, mimeType: f.mediaType, filename: f.filename };
+ return { type: 'file' as const, data: f.data, mediaType: f.mediaType, filename: f.filename };
```

## Context

The `files` parameter was added in #13574, but the file part
construction used `mimeType` (V4 format) instead of `mediaType` (V5
format). The AI SDK v5 `FilePart` type expects `mediaType`, so using
`mimeType` causes the part to not match the expected shape and fall
through to the legacy adapter path.

This bug is present in `@mastra/core@1.9.0` on npm.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Corrected file attachment metadata naming in the message system to
ensure files are properly routed and processed by supported AI SDK
providers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants