Skip to content

fix: lazy-load large attachments in history response#5019

Merged
siddseethepalli merged 1 commit into
mainfrom
do/lazy-load-large-attachments
Feb 19, 2026
Merged

fix: lazy-load large attachments in history response#5019
siddseethepalli merged 1 commit into
mainfrom
do/lazy-load-large-attachments

Conversation

@siddseethepalli
Copy link
Copy Markdown
Contributor

@siddseethepalli siddseethepalli commented Feb 19, 2026

Summary

  • Conversations with large video attachments (~12MB base64) failed to load from history because the history_response JSON payload was too large for the client to decode reliably
  • Daemon now skips embedding base64 data for attachments >512KB, sending metadata (sizeBytes) instead
  • Client fetches large attachment data lazily via the HTTP endpoint when the user clicks play

🤖 Generated with Claude Code


Open with Devin

…oading failure

Co-Authored-By: Claude <noreply@anthropic.com>
@siddseethepalli siddseethepalli merged commit ea47567 into main Feb 19, 2026
@siddseethepalli siddseethepalli deleted the do/lazy-load-large-attachments branch February 19, 2026 07:26
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +163 to +165
do {
let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId)
guard let data = Data(base64Encoded: base64) else { return }
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.

🔴 isLoading never reset when base64 decode fails in openInExternalPlayer lazy-load path

When a lazy-loaded video attachment is opened in the external player and the fetched base64 data is invalid (i.e. Data(base64Encoded:) returns nil), the early return exits the Task closure without ever resetting isLoading to false. The UI gets permanently stuck showing the "Loading video..." spinner with no way for the user to recover.

Root Cause

In openInExternalPlayer(), the lazy-load path sets isLoading = true at line 161, then starts an async Task. Inside the do block, after successfully fetching the base64 string, line 165 has:

guard let data = Data(base64Encoded: base64) else { return }

If the base64 decode fails, return exits the entire Task closure. The isLoading = false at line 169 (success path) is skipped, and the catch block at line 172 is not triggered because no error was thrown.

Compare with fetchAndPlay() at InlineVideoAttachmentView.swift:134-155, which correctly handles all paths: isLoading = false is set in both the MainActor.run success block and the catch block, and the base64 decode failure is handled inside playFromBase64 which sets failed = true.

Impact: The user sees an infinite loading spinner and cannot retry or interact with the video attachment. The only way to recover is to navigate away and back.

Suggested change
do {
let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId)
guard let data = Data(base64Encoded: base64) else { return }
let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId)
guard let data = Data(base64Encoded: base64) else {
await MainActor.run { isLoading = false; failed = true }
return
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 640e401aed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +187 to +192
guard let token = readSessionToken() else {
throw URLError(.userAuthenticationRequired)
}
let url = URL(string: "http://localhost:\(port)/v1/attachments/\(attachmentId)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use HTTP bearer token when fetching lazy attachments

fetchAttachmentData reads ~/.vellum/session-token via readSessionToken() and sends it as the HTTP bearer token, but the runtime HTTP server authenticates against its separate HTTP token (RUNTIME_PROXY_BEARER_TOKEN/http-token) as set in assistant/src/daemon/lifecycle.ts and checked in assistant/src/runtime/http-server.ts. Because those tokens are generated independently, lazy attachment requests will typically get 401 Unauthorized, so large history videos cannot load.

Useful? React with 👍 / 👎.

guard let token = readSessionToken() else {
throw URLError(.userAuthenticationRequired)
}
let url = URL(string: "http://localhost:\(port)/v1/attachments/\(attachmentId)")!
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Query attachment endpoint with the originating assistant scope

This new request path always calls /v1/attachments/:id, which dispatches as assistantId=self in the runtime router, but desktop IPC sessions persist assistant outputs under this.assistantId ?? 'local-assistant' (assistant/src/daemon/session.ts). Even if auth succeeds, the scoped lookup in handleGetAttachment will return 404 for those history attachments, so lazy-load playback still fails; the request needs the correct assistant scope (or an unscoped retrieval path).

Useful? React with 👍 / 👎.

Comment on lines +359 to +360
data: a.dataBase64.length > MAX_INLINE_B64_SIZE ? '' : a.dataBase64,
...(a.dataBase64.length > MAX_INLINE_B64_SIZE ? { sizeBytes: a.sizeBytes } : {}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep non-video attachment payloads inline until lazy loaders exist

The history handler now strips base64 data for every attachment above 512KB, but only video attachments gained a lazy-fetch path in this commit. Existing image/file rendering in ChatView still depends on inline attachment.data (thumbnail/open-in-preview/file-size paths), so large non-video history attachments become unusable or misrepresented (for example, falling back to generic chips with 0 B). Limit omission to video MIME types for now, or add lazy fetch support for the other attachment UIs before dropping inline data.

Useful? React with 👍 / 👎.

@siddseethepalli
Copy link
Copy Markdown
Contributor Author

Addressed in #5053

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.

1 participant