feat(core): recover sessions from rejected attachments - #53014
Draft
kitlangton wants to merge 1 commit into
Draft
kitlangton wants to merge 1 commit into
kitlangton wants to merge 1 commit into
Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A session became permanently unusable after the
readtool stored a truncated PDF from an interrupted download as a successful tool result. Every later request resent it, and the provider rejected each one with HTTP 400invalid_file("The file you uploaded is badly formatted or corrupted"). Retrying, continuing, and compacting all resend the same history, so the only escape was abandoning the session. No validator can catch every bad file, and a valid file can still be rejected by a particular provider, so this adds a general, user-confirmed recovery path.What Changes
Before:
Error: The file you uploaded is badly formatted or corrupted…, with no way forward. Every retry or new prompt fails the same way.After:
provider.media-rejectederror that names the suspect attachments:/continue-without-attachments(also in the command palette while the latest failure is a rejection) lists exactly what will be excluded:Classification
classifyProviderFailureadds anInvalidRequestErrorclassification,media-rejected. It applies to client errors (4xx, or no status for stream errors) only, after the context-overflow, payload-size, and content-policy checks, so those keep their existing handling. Signals:invalid_file,invalid_image,invalid_image_format,invalid_image_url,image_parse_error, plus their messages ("badly formatted or corrupted", "unsupported image", "Invalid image data.").messages.N.content.M.image|document.source…, plus "Could not process image", "does not appear to be a valid png image", and "The PDF specified was not valid".InvalidRequeststays non-retryable, so nothing retries automatically.The Anthropic and Gemini phrasings come from public docs and reported errors. The repo had no recorded fixtures for them, so all tests use synthetic payloads.
Attachment Exclusion
Attribution. A request the provider accepted contained every attachment before the assistant message it produced. So when model M rejects an attachment, the suspects are the attachments M has not accepted yet. That means media in tool results of M's latest accepted assistant message (one with output or usage) and media in any later message. In the incident, that is exactly the PDF the last successful step read. If M never accepted a request (a new session, or right after a model switch), every attachment in the active history is a suspect. The candidate endpoint and the TUI dialog list exactly the set that will be excluded, so nothing is guessed silently. A healthy file added in the same window is also a suspect, because a provider does not say which file failed. The API accepts any subset of refs if a client wants per-file choice.
Durable fact. One new durable event,
session.attachments.excluded { attachments: [{ messageID, callID?, index }] }, names user files (files[index]) or tool-result files (content[index]ofcallID).Projection. The projector records the excluded positions on the referenced item as read-model fields:
User.excludedFilesandAssistantTool.excludedContent. Content is never touched. Because the marks live on the message data, forks copy them even though forks remap message IDs. Every history subset also carries them, including compaction'solderslice andrecentUserMessages. The Solid client store mirrors the event.Filtering.
toLLMMessagesswaps each excluded media part or tool-result file for the note. Every model request is built through it: primary steps, summary and native compaction,generate, and prompt estimation. The existingreplaceMedia,unsupportedParts, andboundImagesseams inmodel-request.tswere not reused. They operate on loweredLLMRequestmessages, where tool results no longer carry the session message ID or the content index, so they cannot target one specific attachment.Action.
POST /api/session/:id/attachment/excludereturnsSessionBusyErrorwhile the session is running. It rejects refs that do not name media in the active history (InvalidRequestError), publishes the event, and resumes unlessresume: false.GET /api/session/:id/attachment/candidatesreturns the suspects withmimeandname. The client SDK was regenerated withbun run generate.Demo
No TUI recording was made. The change adds a hint line under the failed assistant's error, a
/continue-without-attachmentscommand gated on the latest failure being a rejection, and a confirm dialog built with the existingDialogConfirm. I verified it only with the type checker, not by running the TUI against a rejecting provider. Anopencode-drivebefore/after clip with a simulatedinvalid_fileprovider is still needed before this leaves draft.Scope
This PR owns classification, the durable exclusion, request filtering, the API, and the TUI affordance. Follow-ups:
packages/app) shows the descriptive error but has no button yet.[Attached application/pdf: …]for excluded files. The payload itself is never sent.Verification
The new end-to-end runner scenario:
[text, corrupt report.pdf, healthy notes.pdf], and the next request fails withmedia-rejected.report.pdfand resuming sends[text, note, notes.pdf], with no tool re-execution.Unit tests cover: