mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 04:37:37 +00:00
✏️ feat: Edit Pasted Text and Clear It on New Chat (#15017)
* fix: stop an unsent paste from following every new chat An explicit new chat now drops the unsaved-chat draft key before the composer resets. `newConversation` empties the composer, but the key outlives it and `useAutoSave` restores from that key on the way in, so a long paste that was never sent came back as an attachment on every later new chat. Per-conversation drafts are untouched. Clicking a pasted-text chip opens the text in an editor so it can be corrected before sending, and the chip's subtitle offers returning the paste to the composer. The text comes from the in-memory blob, falling back to the `text` field the file record already carries, so neither needs a new endpoint. `FileContainer` grows a `subtitleAction` prop for the second control. Supplying it swaps the chip's own button wrapper for a full-bleed one behind the content, since a button inside a button is invalid markup and browsers drop the inner one's events. * chore: sort imports to fix static checks * fix: address paste edit review findings - Keep the original paste attached until the replacement upload succeeds, so a rejected or failed save cannot destroy the only copy - Guard edits and queued replacements against conversation switches and new-chat resets, mirroring the long-paste lifecycle guards - Recover text for restored pastes by downloading the stored bytes, which Assistants and agent uploads persist without a text field - Delete uploaded attachments when an explicit new chat discards the draft that referenced them, instead of orphaning the records - Mark paste provenance explicitly (session registry plus files draft) instead of inferring it from a filename a deliberate upload can share - Keep the subtitle action revealed on devices without hover * fix: scope draft cleanup to its tab and delete restored pastes - Stamp unsaved-chat files drafts with the writing tab's session id and skip deletion when another tab owns the record, so a new chat in one tab cannot discard the uploads attached in another - Delete a restored paste's upload when an edit replaces it or returns it to the composer, which the attached flag otherwise preserved * fix: harden paste edit lifecycle guards - Re-check the originating composer and the file map before detaching an edited original on upload success, so navigation or a send during the request cannot remove or delete a file the old draft or sent message still references - Record the replacement upload's paste provenance in the session registry and the files draft, keeping Edit and Move back on the new chip - Bind move-inline to the unsaved-chat token as well, and abort the move when the chip is no longer attached to an unsent composer - Restrict new-chat draft cleanup to ids the composer still owns: library re-attaches and ids with unknowable ownership are spared, and uploads still in flight are deleted once their records reach the files cache, unless the file came back attached in the meantime * fix: match restored paste identities and recheck before opening the editor - Treat a chip as attached when any of its ids (map key, file id, temp id) matches the composer map, since draft restoration keys entries by their temporary upload id while the value carries the server-assigned one; the previous key-only check made Move back silently do nothing and left both chips attached after an edit - Recheck the originating composer and the attachment map after the text resolve before opening the editor, so a send during the download cannot stage a replacement upload into the emptied composer - Extend the provenance predicate to temp ids for the same restored shape * fix: spare re-attached sent pastes and discard stale editor resolves - Force-delete a restored paste only when the composer's own draft claims its id, so a paste that was already sent and re-attached from the library keeps its shared record through Edit and Move back - Sequence editor-open requests so a slow text resolve cannot overwrite the chip a later click selected * fix: carry deferred discards across resets and clear the pending draft - Merge newly deferred upload ids with the pending set instead of replacing it, so a second reset cannot orphan an earlier in-flight upload's eventual record - Match deferred ids against temp_file_id as well, since the files cache keys records by the server id while the discard tracked the request uuid - Clear the pane's pending draft key on an explicit new chat, or a running response's queued text and attachments come back with the next run * fix: mint a fresh tab id when sessionStorage was inherited Duplicated and opener-created tabs start with a copy of the original's sessionStorage, so a stored tab id only proves continuity when the document is a reload of the same tab. Every other entry into a document now mints a fresh id, keeping an inherited one from attributing another tab's live drafts to this composer. * fix: gate restored-paste deletion on draft tab ownership and unclip the chip focus ring - Stamp every files draft with the writing tab, not just unsaved-chat ones, and require the stamp to match before a restored paste's record is deleted, so another tab restoring the same draft is not destroyed - Draw the full-chip Edit control's focus indicator as an inset ring with the surface's radius, since the offset ring was clipped away by the surface's overflow-hidden * fix: resolve paste ownership before restoration and across draft migration - Treat a draft's own pastedTextIds as composer-owned at discard time, so a reload-then-new-chat click deletes or defers those uploads before the composer map has been rebuilt, instead of skipping them - Read both the pending and idle draft keys when claiming a restored paste for deletion, since a response finishing mid-edit migrates the record between them * fix: keep failed edits recoverable and retry failed draft deletions - Reopen the paste editor with the user's corrections when the replacement upload is rejected or fails, instead of leaving only the original's text to reopen - Retain deferred and immediate discard ids when the delete request fails, and retry them on the next files-cache update, so an offline or transient failure cannot orphan the uploads * fix: retry restored-paste deletions that fail A failed delete of a detached restored paste retains its payload in a session store, and the discard retry effect drains retained payloads alongside its own batch on every files-cache update, so an offline or transient failure cannot orphan the upload once its chip is gone * fix: queue failed edits and lock chips with actions in flight - Queue a failed edit behind whatever dialog is open instead of dropping its corrections, and reopen it when that dialog closes - Track an in-flight action per source paste and hide its Edit and Move back affordances until the replacement uploads or the move settles, so the same original cannot be acted on twice * fix: address PR review bot findings Codex: - Keep the tab id on back-forward restoration - Preserve the original tab owner when rewriting drafts - Skip clearing idle and pending drafts another tab still owns - Delete pending-draft uploads before clearing them - Return failedFileIds from DELETE /files and retry those records - Spare reattached files from retained deletion retries - Trigger retained-deletion retries when a delete is retained - Persist deferred discards across reload - Delete embedded owned uploads with a discarded draft - Ignore stale paste-editor failures before toasting - Abort a queued edit after the original is sent - Serialize Move back with a synchronous in-flight lock - Prune paste provenance ids that left the draft * fix: retain paste deletions the server reports as failed The delete route answers 200 with `failedFileIds` when a record's storage delete fails, so the detach path's `.catch()` never fired and the orphaned upload lost its only cleanup reference once the draft provenance was pruned. Inspect the resolved response and retain the deletion when it names the file. Extract the `failedFileIds` reader `useNewChat` already had into the file utils so both deletion paths read the response the same way, and give the paste editor coverage for the failed and accepted responses. Also add the missing `size` on a composer file literal that was failing the client type-check. * fix: release draft claims when their tab is gone and keep cleanup durable A tab stamped its id on a files draft and nothing ever took it off, so a draft saved in a tab the user then closed became unreachable for good: no other tab would restore it, write to it, or clean it up, and the closed tab's id can never be presented again. Tabs now report themselves in a small liveness registry and release the claim on pagehide, and a claim whose tab is no longer around is treated as free. Writers restamp a dead claim rather than preserving it. Ownership also only existed once something was attached, so a typed-but-unattached draft on a shared composer key read as nobody's and another tab's New Chat cleared it. Saving text to one of those keys now claims it the same way. Two more from the same review: - The delete route answers a partial failure as 200, so treating an id as still present unless the response reports it deleted kept a ghost row for a file another tab had already removed. Read it the other way around: only a reported failure keeps a record cached. - A retained deletion whose retry failed again moved no effect dependency, so it was never attempted a second time, and the payload only lived in memory. It is now persisted for the session and asks for a backed-off retry, plus one on regaining connectivity. * fix: keep bfcache claims, retain failed deletes, and move the delete contract to TS Four findings from the latest review round: - pagehide fires with persisted: true when a document enters the back-forward cache rather than closing. Releasing the tab's claim there let another tab take the draft and delete files the restorable document still had attached, so the claim is now only handed back on a real unload; a bfcached tab that is never restored still ages out through the liveness window. - useFileDeletion issued its batch and never looked at the outcome, so a fresh paste whose delete failed was orphaned with no retry. It now retains whatever the server did not delete, reading failedFileIds as well as the rejection. - A partial failure answers 200, so the unconditional success toast told the user a file was deleted while it was still on disk and back in their list. - The delete response contract lived in the legacy JS route. It moves to packages/api as buildDeleteFilesResponse, leaving the route a thin caller. The useFileDeletion spec's mutateAsync mock returned undefined; react-query always hands back a promise, so it now resolves like the real one. * fix: park bfcached tab claims and drop ownership left by an emptied draft A document in the back-forward cache has a frozen heartbeat, so the ordinary liveness window expired its claim after 150s even though it could still be restored with those attachments on screen, letting another tab take the draft and delete the files underneath it. Entering the cache now parks the tab as suspended, which holds the claim for 30 minutes: comfortably past the point a browser keeps a bfcache entry, and still bounded, since a claim that never expires is what stranded drafts under owners that no longer existed. Restoring the document beats normally again and clears the flag. The registry entry grew a shape for this and still reads records written as a bare timestamp. Clearing the text of a shared composer key also left the ownership-only record behind, locking the key to a tab with nothing in it: the next tab to type there could neither restore its own draft nor take the key back. That claim is now released when the text goes and nothing is attached. * fix: keep unlinks out of the delete retry and claim shared text before writing Four findings from the latest round: - Retaining a failed agent or assistant unlink sent it through the generic retry, which replays files alone. That drops the tool_resource context, so the route would take its ordinary delete branch and destroy a record the agent and other references still point at. A failed unlink orphans nothing, so those deletions are simply not queued. - A retry that resolved naming files in failedFileIds left both stores untouched, so nothing moved the effect that would try again. It now asks for another attempt on a reported failure, the same as on a rejection. - The reattachment guard read only the idle new-chat key. After a reload the composer map is empty until the autosave restore renders, so a file the user had reattached to the conversation they were viewing, or to the pending key, could be deleted underneath them. All three keys are checked now, including their paste provenance. - Text was written to a shared composer key before ownership was resolved, so a tab could overwrite another's saved text and still be refused the claim, leaving it unable to restore what it had just typed. The claim is taken first, and a claim with no attachment behind it follows whoever's text is actually stored; one backed by an attachment stays with its open owner. * fix: merge shared discard state and keep restored file-search pastes retrievable Four findings from the latest round: - Every mount of useNewChat (header, sidebar, mobile bar, shortcuts) kept its own snapshot of the pending-discard list and wrote it back over one shared session store, so an id recorded by one instance was dropped by the next write from another, orphaning the upload it pointed at. An update now only resolves the ids that instance knows about and carries the rest through. - Refusing an attachment-backed claim still let the text write land, destroying the owning tab's text for a tab that could not have restored it anyway. The claim now reports whether it succeeded and the write is dropped with it. - A restored paste has no tool_resource on its record, so an edit to one that had been uploaded for file search was re-uploaded as a plain context file and the vector-backed original detached, dropping it out of retrieval. embedded does survive on the record and is only set for a vectorized file, so it is what the destination falls back to. - The reattachment guard collected map keys and server ids but not temp_file_id, while the retry lookup resolves that alias: reattaching a file whose discard was pending under its temporary id would not have protected it. * fix: stop the draft owner refusing its own writes and guard shared pending keys Three findings, the first a regression from the previous commit: - The attachment-backed refusal was evaluated before the owner check, so the tab that owned the draft was refused its own key: once anything was attached, nothing typed after it was saved. Ownership is settled first now, and the refusal applies only to another live tab. - A long paste wrote its provenance and pending-paste record into the shared composer key without checking who owned it, and setFilesDraft preserves the existing owner rather than rejecting the write, so the paste was recorded into another tab's draft, which could then restore and delete the upload while this tab still showed the chip. Both write sites now check first. - Two concurrent runs share the default pending key, and the migration to the new conversation ran before the ownership check: the finishing run moved the other tab's text and attachments under its own conversation and left that tab nothing to carry over. Ownership of the source is verified before migrating, and this composer's own text is still saved either way. * fix: protect cross-tab reattachments and orphaned pastes on every discard path Five findings from the latest round: - The retry guard only read this pane's own draft keys, so a file a second tab had reattached to a conversation this pane never opened was deleted anyway. Drafts live in localStorage and are readable from every tab, so the guard now sweeps every persisted files draft rather than three known keys. - Clearing the composer removed the shared text record without checking who owned it, so an empty composer in one tab erased text another tab was still holding behind its attachments. The clear path takes the same guard as the write path, and both now share one ownership predicate. - When another tab owned the pending key, this tab's own queued attachments were cleared from the map and never written anywhere, because the autosave that would have persisted them had been refused that key for the whole run. They are now written under the conversation the run just became. - A draft write that storage refuses (private mode, quota) left a generated paste with no record to discard it by. New Chat now also collects the live marked pastes the composer is still showing, skipping re-attached ones. - With draft saving off, the reset path deleted files without awaiting or reading the response, so a failure orphaned the upload. It retains what the server did not delete, like every other deletion path. * fix: keep reloading tabs live and spare pastes an active run is using Three findings: - pagehide cannot tell a reload from a close, and the tab id survives a reload on purpose, so releasing the claim there handed this tab's own draft to another one while the document was still bootstrapping. A closing tab is left to the ordinary liveness window instead, which is what the window is for. Entering the back-forward cache is still marked, since that heartbeat freezes. - The text-ownership guard only covered the shared composer keys, but a conversation key is reachable from every tab viewing that chat and is stamped the same way, so one tab could overwrite text another was holding behind its attachments. The guard now applies to any key; the ownership stub is still only created for the shared keys, which tabs otherwise share freely. - Submitting empties the file map but leaves the draft's paste provenance until the final SSE event, so New Chat during a streaming response treated the empty composer as still owning what the message had just sent and deleted files the message, and the run reading them, still referenced. The provenance promotion is skipped while a run is in flight. * fix: give each tab its own presence record and publish live attachments Four findings: - Tab presence lived in one shared localStorage map, so two tabs beating at the same time read the same snapshot and wrote back rival copies; the loser disappeared until its next beat, long enough for another tab to treat its live draft as abandoned. Each tab now writes only its own key, and expired records are swept while reading. - With draft saving off nothing is written to a draft at all, so a file reattached in another tab was invisible to a retry running here and could be deleted underneath it. A tab now publishes what its composers are holding into its own presence record, and cleanup unions that with the drafted ids. - The record written when another tab owns the pending key kept only attachment ids, so a restored chip stopped being recognised as a paste and lost editing and cleanup. Provenance is rebuilt from the session registry. The unsent paste text cannot come along: this tab was refused that key all run, so it was never stored anywhere to carry. - New Chat with draft saving off skipped every embedded record, leaving an unsent file-search paste with its metadata, storage and vectors intact. A paste this composer owns is now included with its real embedded value, while other embedded files are still left alone. * fix: elect one cleanup worker, scope the queue to its account, guard edit writes Three findings: - Every mounted useNewChat (header, sidebar, mobile bar, shortcuts) entered the cleanup effect against one shared store, so a single retry issued the same DELETE several times and toasted about each. A pass is now claimed before it runs; an instance that is turned away asks for a later one rather than dropping the work. - The retained queue outlived a sign-out, so the next account retried the first one's payloads, was refused by the ownership check, and rescheduled forever. Logging out clears the queue and cancels the pending retry. - The paste path checks draft ownership before recording provenance, but the edit path did not, so a replacement could be written into a record another open tab owns, which that tab could then delete while this one still showed the chip. It takes the same check. * fix: match paste identities everywhere and stop migrations clobbering a foreign draft Five findings: - The presence record published only composer map keys, but a restored upload is keyed by its temporary id while the value carries the server one, and a retained deletion in another tab names whichever it recorded. All three identities are published now, matching the local guard. - Migrating a finished run checked that the pending record was ours but not the destination, so a conversation draft another tab owned with attachments on screen was overwritten and restamped. Both ends are checked, and the non-owner fallback no longer writes over a foreign destination either. - The live-paste fallback matched the registry against file_id alone, so a completed paste, marked under its client upload id, read as somebody else's file and its upload survived New Chat. It matches every identity now. - An upload still in flight has no filepath or source, so no discard path can build a payload and the reset drops the chip anyway. Its id is deferred so the record is deleted when it arrives, with draft saving on or off. - An edited paste that had been staged into the code sandbox was re-uploaded as a plain context file, since only the file-search case was reconstructed. metadata.codeEnvRef is durable and now routes it back to execute_code. * fix: silence background cleanup and keep cross-tab protection past a send Four findings: - The reset path matched the paste registry on file_id alone, the same alias gap already fixed in New Chat, so a completed embedded paste read as somebody else's file and survived with its vectors. It matches every identity now. - The background cleanup pass used the ordinary delete mutation, so a storage failure that kept failing announced itself on every retry, and success arrived minutes after the action behind it. The mutation takes a silent option and the retry pass uses it; direct user actions still report. - Each hook instance loaded the pending-discard list once and was never told when another instance wrote it, so work deferred by an instance that then unmounted stalled. Writes now notify every mounted instance, which re-read and apply only a real change. - Cross-tab protection sampled only what a composer was holding right then, and sending clears both the map and the draft, so a file reattached in another tab and then sent could be deleted between retries. A tab now remembers what it recently held for ten minutes, which is long enough for the other tab's next pass to see it and cancel that deletion for good. * fix: honour draft ownership in every clear and track what a message consumed Five findings: - The ownership contract was only applied at the new call sites; the SSE final event, the steering handoff and the debounced text clear still erased records through clearAllDrafts and clearDraft. The check moved inside those helpers, so every path that clears a draft respects it. - Only the explicit logout cleared the retained queue, leaving a silent refresh that returns nothing and a failed user query to carry it into the next account. It clears wherever the session is lost instead, in the one place all three paths pass through. - Using isSubmitting to decide whether a paste was consumed was wrong for a stopped or errored turn: those clear the flag without clearing the draft, so New Chat afterwards deleted files the turn already referenced. Submission now records the ids it took, and those are excluded by name. - The presence sweep only ran from deletion cleanup, so a profile that never had a failed delete accumulated a record per tab until the origin quota ran out and draft writes began failing silently. The heartbeat sweeps. - When a run finished into a conversation another tab owned, this tab's own queued text and attachments were dropped for want of a writable destination. They stay on the key it does own and are restored from there. * fix: mint a tab id when the browser has no randomUUID crypto.randomUUID is absent on insecure origins and in older webviews, and the throw left the tab with an empty identity: every draft was then written without an owner and every ownership guard read another tab's record as its own, reinstating exactly the loss this layer exists to prevent. Falls back to getRandomValues, then to a local mint. The id only has to tell tabs apart. * fix: address PR review bot findings Clear the retained deletion queue on every direct authentication exit, not just the debounced context update: an empty or rejected silent refresh, a failed user query, and the external-IdP logout all leave the page without passing through setUserContext, so the queue survived into whoever signed in next and retried under credentials the ownership check rejects forever. Settle the edit lock when a replacement upload is aborted. Removing the replacement chip mid-upload consumes the lifecycle through onAbort, which the paste editor never handled, so the source paste kept its Edit and Move-back actions hidden for the rest of the session and the typed correction was lost. Keep the temporary-file cleanup payload for whatever the server reports as failed. The delete route answers a partial storage failure with a 200 carrying failedFileIds, and the cleanup mutation cleared FILES_TO_DELETE wholesale on any success, dropping the only automatic retry those orphans had. Persist both paste registries per tab. They lived in module-level sets, so a reload kept the files draft but forgot the paste had been consumed, and New Chat then classified an already-sent paste as unsent and deleted a file the persisted message still references. Withdraw discarded ids from tab presence. A removed, moved, or discarded chip kept its recent entry for the whole window, and the retry sweep read that as evidence the file had been reattached: it cancelled its own cleanup and left the failed upload orphaned on the server. Presence records whose heartbeat cannot be read are skipped rather than rewritten, since giving one a fresh seenAt would revive a dead tab's claims over every id it still held. * fix: address second round of PR review bot findings Stop a settled deletion from undoing the logout clear. A DELETE that was already in flight when the session ended settles afterwards, and its handler is the last reference to that payload, so it wrote the departing account's records straight back into session storage. Clearing now latches retention shut and only a newly established session reopens it, which also covers the paste editor's own retention and the discard paths, not just this one writer. Reinsert a failed paste when nothing durable holds it. A composer the user has typed into is deliberately left alone while a recovery record exists, because that record restores at an anchored offset later. When the shared draft key belongs to another live tab the guard skips the record entirely, so the upload callback held the only copy and refusing dropped the text outright. It now goes back in at the offset its anchors resolve to, which is where a restore from a record would have put it. * fix: address third round of PR review bot findings Withdraw attachment presence from this tab only. The sweep cleared the withdrawn ids out of every tab's recent map, which is the one record a second tab has left once it has reattached a file and sent it: its composer and its draft are both empty by then, so erasing that entry handed the next retry a file it read as abandoned and let it delete the upload out of the message now referencing it. The withdrawing tab always published what it withdraws, so its own record is all it needs to touch. Guard the direct New Chat deletion against other tabs. The retry effect consults every other tab's drafts and published presence before deleting, but the discard that runs on New Chat went straight to the request, so it raced past that guard and could delete a file another tab still had attached or had already sent. It now consults the same two sources, excluding its own draft keys and its own presence record, which hold exactly what the discard is throwing away. Fix the import order in Presentation.tsx, which CI static checks flagged. * fix: address fourth round of PR review bot findings Read this tab's presence before sweeping stale keys. Timers pause while the machine sleeps, so a live tab can beat again with its own record already past the liveness window; the sweep reaped it and the write that followed published an empty presence, and nothing republished it because the file map had not changed. Another tab's retry then saw no claim on chips this one still had on screen. Keep submitted-use evidence when a later chip is withdrawn. The same file can be sent on one message and reattached afterwards, and once the composer and draft have cleared, its recent entry is the only cross-tab record that a message still references it. Withdrawing a chip no longer erases an entry for an id a submission already consumed; it ages out on the ordinary window. Clear composer drafts when the account changes. A files draft carries the whole text of a paste held as a file, and the browser tab keeps its identity across an in-app account switch, so the ordinary draft restore could hand the next account the previous one's writing. Both draft families are now dropped on the sign-in and sign-out paths, ahead of the skipFirst exception. Spare a submitted paste from the edit path's explicit deletion. Editing or moving a reattached library file that an earlier message sent deleted the server record underneath that message, because the draft-ownership check succeeded and nothing consulted the submitted marker. Validate an edited paste as a replacement rather than an extra file. The original is deliberately still attached while the replacement uploads, so the shared validation counted both and rejected the edit at the file-count or total-size limit; with a limit of one, a lone paste could never be edited. Preserve failed rows after a table deletion. The table's own cache update removed every requested file without consulting failedFileIds, undoing the partial-aware update and hiding a file whose storage delete had failed. Document the two deliberate dependency omissions in AuthContext, which CI now lints at zero warnings because the file is part of this change. * fix: address fifth round of PR review bot findings Clear composer drafts on the way out of a session, not only on the way in. Clearing them from the login mutation missed social sign-in entirely: OAuth, OpenID and SAML leave through direct links and come back through the silent refresh, so a different account could arrive in the same tab with the previous account's drafts and tab identity intact and have its paste text restored. The draft clearing is now paired with the retained-deletion clearing in one helper used by every authentication exit, so neither can be wired into a path the other was missed from. Rebuild paste provenance when restoring a queued upload. A paste queued during a run has its pending draft taken by takeComposerDraft, so choosing Edit message restored the upload into an empty composer with nothing recording that it was a generated paste. Filtering existing provenance could not recover that, and an unmarked restored chip is treated as a shared attachment: removing it would not delete it and New Chat skipped it, orphaning the unsent upload. The session registry still knows, so it is consulted. Drop paste provenance when a rejected upload is removed. Validation can reject a paste before it reaches composer state, and the failure path removes it with removeFile, but the id stayed in pastedTextIds. That left a record hasDraftAttachments reads as a real attachment claim with no chip behind it, and with the file map unchanged nothing pruned it, so it locked every other tab out of the shared composer key. * fix: address sixth round of PR review bot findings Centralise the foreign-claim guard. Every path that deletes an upload has to ask whether another tab or pane still claims the file, and the guard was being assembled by hand at each site, which is exactly why it was missing from three of them. collectForeignAttachmentClaims now builds that set once, and the New Chat discard, the no-draft reset fallback in useNewConvo, and the paste editor's explicit deletion all consult it. A record another tab claims is skipped rather than retained, since it was never this pane's to delete. Scope presence withdrawal to the pane that owns it. One tab holds several composers and the presence record is flat, so the hook that won the global deletion pass swept every pane's entry while knowing only its own file map, erasing the evidence of a chip a sibling pane still had on screen. Withdrawal now takes the pane index, and an id another pane still lists keeps its recent entry too. Guard destructive draft clearing against text-only claims. claimComposerDraftTab stamps a key that holds nothing but text, and the write guard ignores a claim with no attachment behind it, so a tab finishing a run that began as an unsaved chat cleared the shared new-chat key and took another tab's half-written message with it. Clearing now honours any live foreign stamp, while text writes keep their deliberate last-writer-wins behaviour. Mark queued override files as submitted. A during-run queued message drains through overrideFiles into the reuseFiles branch and skipped the marker loop entirely, so reattaching that paste later left isPasteSubmitted false and New Chat or an edit could delete a file the queued message still referenced. * fix: address seventh round of PR review bot findings Treat publishing an attachment as proof of liveness. The publisher carried the old seenAt over, so a tab whose timers had been paused past the liveness window published a chip and stayed expired until its next interval tick, long enough for another tab's cleanup to sweep the record and delete the file under the chip that had just appeared. Count a sibling pane as a claim. The foreign-claim helper excluded this tab entirely, so the pane doing the discarding could not see the other composer in the same tab and deleted a file it still had on screen. Live claims are now gathered per pane: only the discarding pane's own entry is left out, along with this tab's recent map, which is flat and cannot say which pane an id came from. Keep a tab identity when session storage is unusable. It can be blocked or full while localStorage still works, and returning an empty id left the document unattributed, which every ownership and liveness guard reads as no owner, so tabs could destructively clear each other's attachment-backed drafts. An id that lives only for this document still tells the open tabs apart. Scope attachment withdrawal in the deletion hook to the originating pane, and thread the composer index through ChatForm, FileFormChat and FileRow to supply it. Removing a file from one side-by-side composer withdrew the id for every pane, and with drafts off the sibling never republished its claim. Check foreign claims before deleting a live edit source. The guard only covered the restored path, because detach returns early for an in-memory upload before reaching it, so editing a live paste another tab had reattached from the library deleted the file underneath that tab's chip. Keep autosaving to the pending key while the destination is not writable. The preserved queued work was written under the pending key but the destination was still recorded as the active conversation, so later edits autosaved against a foreign key and a reload mounted straight onto the destination, losing the work that had just been preserved. * fix: address eighth round of PR review bot findings Make submitted-use evidence durable and readable across tabs. The tab that retries a retained deletion is rarely the tab that sent the message, and this evidence lived in the sending tab's session storage, which left published tab presence as the only cross-tab record. That ages out on a fixed ten-minute window, so a retry resuming after a longer freeze classified a sent file as abandoned and deleted it out of its message. It is timestamped in localStorage now, with a horizon wide enough to outlast any plausible freeze and a hard cap so a long-lived profile cannot grow it without bound. Paste provenance stays in session storage, since which chips offer the paste affordances really is per-tab. Tie a blocked pending draft to its intended destination. Keeping the pending key active while a live tab owned the destination left the pending state with no memory of where it was heading, so any later navigation looked like the awaited transition and carried the queued text and attachments into an unrelated conversation. Defer an in-flight paste on direct conversation resets. Callers that reach newConversation without going through New Chat left an upload with no filepath yet unrecorded, so once the request landed nothing remained to delete the server file. * fix: restore the composer clear after send CI e2e caught this: after sending a message with an attachment the chip stayed in the composer, so the sent message and the composer both showed it. Two causes, both from keying composer storage off state that lags a render. `currentConversationId ?? conversationId` is the previous conversation during every transition, so the file-cache restore ran against the outgoing key and put the just-sent attachment straight back into the map that the submit had cleared. The active key is now the conversation unless the switch effect has deliberately parked storage on the pending key. Separately, treating any first mount as the awaited pending transition ran the pending migration on every direct load of a conversation. That is narrowed to a pending record this tab owns which actually holds something, which is what a reload with real queued work looks like. Verified against the two failing specs locally, then the whole mock chat spec: 8 passed. * fix: address ninth round of PR review bot findings Stop expiring submitted-use evidence on a timer. The work it has to outlast is a retained deletion, and those carry no expiry of their own, so any interval chosen could be outlived by a suspended tab still holding cleanup work, which is the same bug with a longer fuse. The ledger is bounded by count instead, evicting oldest first only when it would otherwise grow without limit. Consult that ledger before retrying a deletion. The retry pass built its protection set from drafts and published presence only, both per-tab and time-bounded, so a file sent from a tab that has since been suspended had nothing left to speak for it. The record is resolved before judging, because the discard is often keyed by the temporary upload id while the pane that sent it marked only the server id. Refresh liveness when withdrawing presence, matching the publication side. A retained-deletion pass resuming after paused timers withdrew its own entry and then swept the record as stale, taking sibling panes' claims with it. Keep queued attachments when neither draft key is writable. With another tab owning the pending key and a second owning the destination, the effect cleared the live map and could persist it nowhere, so unsent attachments vanished the moment the run got its conversation id. Remove the replacement provenance when an edited paste is not accepted. The edit path records the replacement id before routing the upload, and a rejected upload left a provenance-only draft that reads as a live attachment claim with no chip behind it. Verified with the mock chat e2e spec after rebuilding the frontend: 8 passed.
This commit is contained in:
parent
21ba9d3f30
commit
124e357cbf
44 changed files with 7047 additions and 159 deletions
|
|
@ -7,6 +7,7 @@ const {
|
|||
getApprovalTtlMs,
|
||||
refreshS3FileUrls,
|
||||
handleFilesUsageRequest,
|
||||
buildDeleteFilesResponse,
|
||||
shouldUseUploadSse,
|
||||
startUploadSseStream,
|
||||
sendUploadPolicyError,
|
||||
|
|
@ -176,6 +177,9 @@ router.post('/usage', async (req, res) => {
|
|||
|
||||
router.delete('/', async (req, res) => {
|
||||
try {
|
||||
const sendDeleteResult = (result, successMessage) =>
|
||||
res.status(200).json(buildDeleteFilesResponse(result, successMessage));
|
||||
|
||||
const { files: _files } = req.body;
|
||||
|
||||
/** @type {MongoFile[]} */
|
||||
|
|
@ -260,14 +264,14 @@ router.delete('/', async (req, res) => {
|
|||
}
|
||||
|
||||
if (dbFiles.length > 0 && nonOwnedFiles.length === 0) {
|
||||
await processDeleteRequest({ req, files: ownedFiles });
|
||||
const result = await processDeleteRequest({ req, files: ownedFiles });
|
||||
logger.debug(
|
||||
`[/files] Files deleted successfully: ${ownedFiles
|
||||
.filter((f) => f.file_id)
|
||||
.map((f) => f.file_id)
|
||||
.join(', ')}`,
|
||||
);
|
||||
res.status(200).json({ message: 'Files deleted successfully' });
|
||||
sendDeleteResult(result, 'Files deleted successfully');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -290,20 +294,19 @@ router.delete('/', async (req, res) => {
|
|||
const toolResourceFiles = assistant.tool_resources?.[req.body.tool_resource]?.file_ids ?? [];
|
||||
const assistantFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
|
||||
|
||||
await processDeleteRequest({ req, files: assistantFiles });
|
||||
res.status(200).json({ message: 'File associations removed successfully from assistant' });
|
||||
const result = await processDeleteRequest({ req, files: assistantFiles });
|
||||
sendDeleteResult(result, 'File associations removed successfully from assistant');
|
||||
return;
|
||||
} else if (
|
||||
req.body.assistant_id &&
|
||||
req.body.files?.[0]?.filepath === EModelEndpoint.azureAssistants
|
||||
) {
|
||||
await processDeleteRequest({ req, files: req.body.files });
|
||||
return res
|
||||
.status(200)
|
||||
.json({ message: 'File associations removed successfully from Azure Assistant' });
|
||||
const result = await processDeleteRequest({ req, files: req.body.files });
|
||||
sendDeleteResult(result, 'File associations removed successfully from Azure Assistant');
|
||||
return;
|
||||
}
|
||||
|
||||
await processDeleteRequest({ req, files: authorizedFiles });
|
||||
const result = await processDeleteRequest({ req, files: authorizedFiles });
|
||||
|
||||
logger.debug(
|
||||
`[/files] Files deleted successfully: ${authorizedFiles
|
||||
|
|
@ -311,7 +314,7 @@ router.delete('/', async (req, res) => {
|
|||
.map((f) => f.file_id)
|
||||
.join(', ')}`,
|
||||
);
|
||||
res.status(200).json({ message: 'Files deleted successfully' });
|
||||
sendDeleteResult(result, 'Files deleted successfully');
|
||||
} catch (error) {
|
||||
logger.error('[/files] Error deleting files:', error);
|
||||
res.status(400).json({ message: 'Error in request', error: error.message });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue