Uh oh!
There was an error while loading. Please reload this page.
fix(runtime-host): copy user-uploaded attachments when branching a conversation - #4289
Conversation
…nversation Forking/branching a conversation (and the sibling revision / side-conversation copies) that contained a user-uploaded attachment failed every time with `persistence_failed: Conversation copy is missing Session file attachment-<id>`, surfaced to the user as the generic "操作失败 / 任务操作失败,请稍后重试" toast. User uploads are committed as artifact records whose `turnId` is the upload id sentinel, not a conversation turn, so the turn-scoped artifact copy never selected them and `rewriteStorageRef` could not resolve their `session_file` refs — the copy threw and rolled back. Collect the `session_file` attachment ids the copied slice actually references (new `collectConversationCopySessionFileRefs`, walking the same ref sites as `rewriteStorageRef`) and force-include them via a new same-Session `includeArtifactIds` input to `copyConversationArtifacts`, alongside the existing turn-scoped selection. Lenient: unknown ids are a no-op. Fixesapache#4288 Generated-by: Claude Code
Address non-blocking review feedback on apache#4289: - collectConversationCopySessionFileRefs: add an archivedResults image case (and a child-Session archived image that must be ignored), covering the previously-untested archived tool-result branch. - two-client UDS branch E2E: read back the copied attachment bytes and assert the rewritten session_file ref for the branch, the admitted revision, and the active-source side conversation — proving all three copy kinds carry the user-uploaded attachment, not just branch. Refs apache#4288 Generated-by: Claude Code
liuxiaocs7
commented
Aug 30, 2026
Addressed the non-blocking test feedback in 3f7d3eb:
Local: Left the collector/linked-child "shared visitor" refactor out of this PR as a future cleanup, per the non-blocking note. |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for fixing conversation branching through the existing Runtime Host and Artifact Store copy authority. I verified exact head 3f7d3eb: copied-slice reference discovery, target identity rewriting, source and target admission, deterministic retry, rollback, active-source handling, and content readback are all covered, with exact-head checks green. I found no P0–P3 issues. This was an AI-assisted review using Codex and independent reviewer agents; I independently verified the production, recovery, and test paths and own this approval.
Uh oh!
There was an error while loading. Please reload this page.
Summary
Forking/branching a conversation that contains a user-uploaded attachment (image / PDF / file) failed 100% of the time. The Desktop toast was the generic
操作失败 / 任务操作失败,请稍后重试; the Runtime Host log showedRuntimeHostOperationError: Conversation copy is missing Session file attachment-<id>(code: persistence_failed), and the whole copy was rolled back so no branch was created. Text-only conversations were unaffected. The same copy path backs revision and side-conversation copies, so they had the same gap.Root cause: user uploads are committed as artifact records whose
turnIdis the upload id sentinel (artifact-coordinator.ts#commitIngest), not a conversation turn.copyConversationArtifactsselects which session files to copy strictly by conversation turn (turnIds.has(record.turnId)), so a user upload was never selected, its id never entered theartifactIdsmap, andrewriteStorageRefcould not resolve the attachment'ssession_fileref → threw → rollback. (Tool-producedsession_fileimages are turn-scoped, so they were already copied correctly — only user uploads were affected.)Fix: collect the
session_fileattachment ids the copied slice actually references — a newcollectConversationCopySessionFileRefsthat walks exactly the ref sitesrewriteStorageRefis reached from (user-message attachments, tool_result image refs, text runtime-event attachments, function_response / archived tool-result images) — and force-include them via a new same-SessionincludeArtifactIdsinput tocopyConversationArtifacts, alongside the existing turn-scoped selection. The include branch is lenient (an id with no matching source record is a no-op) and de-dups against the turn selection to avoid a double-copy. Applied for all copy kinds (branch, revision, side conversation).This is the same class of "a reference not carried through the conversation-copy path" bug as the already-fixed#2060 / #2061, #3806, and #3776.
Fixes#4288
Verification
packages/storage—includes explicit same-Session Artifacts outside the copied turns: auser_uploadrecord with a sentinelturnIdis absent from a turn-scoped copy and present onceincludeArtifactIdsnames it; unknown ids are a no-op.packages/runtime—collectConversationCopySessionFileRefs gathers source-Session refs across sites: collects source-session refs at all four ref sites and excludes a child-session ref.packages/runtime-host— the two-client UDS branch E2E fixture is now production-faithful (uploadturnId= sentinel, attachmentref.relativePath= record id). I confirmed it fails without the fix with the exact production error (Conversation copy is missing Session file source-artifact,persistence_failed) and passes with it.test:dist):@maka/runtime3090 pass / 0 fail;@maka/runtime-host1427 pass / 0 fail;@maka/storage1006 pass / 1 fail. The single storage failure is the pre-existing, environment-specificmanaged-dependency-environment-crashtest (its spawned child's stderr picks up Node'sExperimentalWarning: SQLite …); it is untouched by this PR and fails the same way on a clean tree.biome checkclean on all changed files;tscbuilds clean for@maka/storage,@maka/runtime, and@maka/runtime-host.AI use
Select exactly one:
Tool(s) and scope: Claude Code (Anthropic) — root-caused the failure, implemented the collector +
includeArtifactIdsplumbing, and wrote the unit and E2E tests. Reviewed by the author. The commit carries aGenerated-by: Claude Codetrailer.Checklist
Does this PR entail a change in behavior?