feat(chat): attach files to question answers - #463
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
Question answers accept pasted images and uploaded files on web, desktop and mobile. The server claims them like turn attachments, records an answer-submitted history activity, and appends the saved file paths to the provider answer strings. Attachment cleanup keeps its own projection cursor so reverts and deletes retain files that answers still reference. Pylon adaptations: the normalizer and failed-dispatch cleanup also keep covering thread.input-queue.follow-up; mobile readiness checks upload states directly because this branch predates the #10404 upload helper. Adopted from 7220dfe2c949476eaa7d21eccbcd3a0ce0eddb49 (#9871)
Bootstrap decoded every event past the cleanup cursor and listed the attachments directory once per deleted or reverted thread, and the live path never advanced the cursor. First start after upgrade replayed the whole log (1,000 deleted threads with 3,000 files took about 28 s), one old payload that no longer decodes stopped the engine from starting, and a persistent file error pinned the cursor. The live path now writes the cleanup cursor with the projector cursors at the last finished cleanup. Bootstrap selects only revert and delete rows past it without decoding payloads, lists the directory once, skips threads without files, and moves the cursor to the projector head. A database without the row starts at its lowest projector cursor. File errors are logged and retried once on the next start instead of pinning the cursor, and revert retention reads only answer activities. Remove the unused minLastAppliedSequence, which would have included the non-projector cleanup row.
…ed pastes A cached thread snapshot can predate a question, so discarding question attachment drafts from it deleted drafts and their local file copies after a cold start or while reconnecting. Only live thread data may discard them now, matching web. Pasting an image into an answer on a server without question attachment support was silently ignored. It now shows the same update reason web shows.
rynfar
force-pushed
the
upstream/2026-09-10-question-attachments
branch
from
September 11, 2026 02:01
8d67f6d to
4ae0123
Compare
This was referenced Sep 11, 2026
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.
When an agent asks a question that accepts a custom answer, there was no way to answer with a screenshot or a file. Pasting an image or attaching a file was refused until the question was answered.
This ports T3 Code's question attachments, pingdotgg/t3code#9871, at the frozen head
6c583620ff7ad3235b135af7107c0543467eecfa(tracking #414). It is the one source in the web-composer lane that crosses contracts, server, web, desktop and mobile, so it is split from #457. Both PRs touchChatView.tsx,ChatComposer.tsx,MessagesTimeline.tsxand the ledger, so whichever merges second needs a rebase.Source
7220dfe2c949476eaa7d21eccbcd3a0ce0eddb49What it does
UserInputAttachmentsandUserInputAttachmentAnswerPayload, an optionalattachmentsByQuestionIdonthread.user-input.respond,thread.user-input-response-requestedandProviderRespondToUserInputInput, and aquestionAttachmentsenvironment capability. Every field is optional, so older clients and servers keep working. Clients hide the option until the server advertises it.user-input.answer-submittedactivity, and for Codex's message-mode questions it carries the files on the answer turn.ProviderServiceadds one line per saved file path to the answer strings, so provider answer protocols do not change.projection.attachment-cleanuprow inprojection_state. No migration.docs/user/question-attachments.md(linked fromdocs/README.md), in Pylon's voice.docs/internals/overview.mddescribes the cleanup cursor.Attachment cleanup: Pylon changes upstream's mechanics
The feature's cleanup behavior is upstream's: after every projector has caught up, reverts remove files no message or answer still references, and deletes remove the thread's files unless the id was re-created. How that cleanup runs is different. Upstream's design at the frozen head has three defects, and nothing after the frozen head changes them:
What Pylon does instead, in
ProjectionPipeline.ts:upsertManystatement as the projector cursors, at the last event whose cleanup finished. Cleanup runs after commit, so the cursor trails the head by the current command. This adds no statements. A failed cleanup stops the cursor until the next start. A transaction that rolls back never ran its cleanup, so it leaves no gap.thread.revertedandthread.deletedrows past the cursor. It readssequence,event_typeandstream_id, never the payload, and logs and skips any row it cannot read. The attachments directory is listed once and grouped by thread segment. Threads without files cost no reads. The cursor then moves to the projector head.user-input.answer-submittedactivities (activityKinds), not every activity payload.Why cleanup still runs inside bootstrap. Running the backlog after the engine is ready would race live commands. A message arriving mid-sweep can reference a file the sweep is about to prune, so the sweep would need its own serialization with the command queue. With the live cursor and the type-filtered scan, blocking work is proportional to real pending cleanup: none on a normal restart, and only threads that still have files after a rebuild. Keeping it in bootstrap is the smallest correct design.
Measured with a throwaway probe (in-memory SQLite, every projector current, same machine). Same probe run against the previous head of this PR and against this head:
For reference, the reviewer measured
origin/pylonat 2 ms and 1 ms for the first and fourth scenarios.ProjectionStateRepository.minLastAppliedSequenceis removed. It had no callers here or upstream, and its minimum over every row would now include the cleanup row.computeSnapshotSequencealready reads only the required projectors.docs/internals/overview.mdrecords thatprojection_statecan hold non-projector rows.Provider decisions
allowCustomAnswer: false, so the client hides attach and the decider refusesuser-input.requestedquestions (its session dialogs userespondToInteraction)Pylon adaptations
normalizeDispatchCommandandcleanupFailedUploadedAttachmentsstill cover Pylon-onlythread.input-queue.follow-upalongsidethread.turn.startand the new respond command.ChatComposer's send gate uses Pylon'sbaseSendDisabledReason. Pylon's pending-question panel already passed only the active request as responding, so that hunk was already covered. Pylon'sdisplayTextand viewed-image expansion in work rows are kept, with answer history added beside them.composerAttachmentsStillUploadingandcomposerAttachmentUploadBlockReason, which landed onpylonwith feat(mobile): keep new-task drafts and queued sends visible #460. The earlier direct check was equivalent and is gone.@types/react-domis added to mobile devDependencies for the render tests. The lockfile entry was regenerated withvp i, not hand-merged.Nothing Pylon-original was removed.
Verification
Rebased onto
origin/pylonatac8d87b63f(after #458, #460 and #467).vp test runon 16 files, 338 tests pass:ProjectionPipeline,ProjectionPipeline.threadHandoff,OrchestrationEngine,ProjectionSnapshotQuery,ProviderCommandReactor,RollbackReconciliation,RollbackAdmissionAtomic,Normalizer,Normalizer.attachments,decider.questionAttachments,decider.userInputDismiss,decider.inputQueue,ProviderService,userInputAttachments,ServerEnvironment,attachmentStore.bounds bootstrap cleanup by threads that still have files(one directory listing and a SQL statement count that does not grow with 200 deleted threads without files; a restart lists nothing),starts when an old event no longer decodes, andcleans past a file that cannot be removed without pinning the cursor.cleans attachments only after the command receipt commitsnow asserts the live cursor, one retry on the next start, and a persistent failure that does not pin the cursor.ChatView.logic,MessagesTimeline,composerDraftStore,attachmentUploadQueue,pendingUserInput,questionAttachments,session-logic.vp test run --dir apps/mobileon 5 files, 133 tests pass:QuestionAnswerHistory,threadActivity,composerAttachmentUploadQueue,use-selected-thread-requests(adds the cached and synchronizing draft cases; they fail without the live gate), and the newQuestionAttachmentspaste test.t3,@t3tools/web,@t3tools/mobile,@t3tools/contractsand@t3tools/client-runtimereport no errors.PRIME_AGENT_DRIVER_KINDusages matchorigin/pylon.vp linton the PR's 40 changed TypeScript files reports nothing, andvp fmt --checkis clean on all 45 changed files. A freshvp ileavespnpm-lock.yamlunchanged.Ported by Claude Opus 5 in Claude Code.