mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🧮 fix: Stop Offsetting Once SYNC Drops the Edited Prefix
The edit offset was applied unconditionally, but whether it is correct depends on which branch SYNC took. SYNC either preserves the content already loaded for the response -- which still contains the retained prefix, so the offset is required -- or replaces it with the server's aggregatedContent, which is completion-local and indexed from zero, after which any offset writes past the end of a now shorter array. That is why the two previous attempts each fixed half of it: skipping the offset on resume was right for the replace branch, applying it unconditionally was right for the preserve branch, and neither holds on its own. The offset now tracks the actual state of the rendered content. For an activity label the replace branch was worse than a bad position: the fill landed past its own reservation, so the pending placeholder was never resolved and the block kept its generic header for the rest of the run. Applied to run steps as well, not just labels. useStepHandler reads the prefix from the same submission and had the same unconditional offset, so after a mid-session resume of an edited response tool cards were misplaced too. Normalizing at the dispatch boundary keeps both in ONE index space by construction: a label that shifted differently from the tools it heads would land on another part. Note the reload path was already coherent -- useResumeOnLoad rebuilds the submission without editedContent or editPrefixLength, giving no offset against server-supplied content -- so only the mid-session SYNC path was inconsistent.
This commit is contained in:
parent
c7db006903
commit
57e45cbb67
1 changed files with 40 additions and 2 deletions
|
|
@ -550,6 +550,13 @@ export default function useResumableSSE(
|
|||
* two retries can't cancel each other. */
|
||||
const steerRetryRef = useRef<number | null>(null);
|
||||
const activityLabelRetryRef = useRef<number | null>(null);
|
||||
/**
|
||||
* Set once a SYNC has replaced the response with the server's
|
||||
* completion-local snapshot, which discards the prefix an edited
|
||||
* resubmission retained. From that point incoming indices are absolute and
|
||||
* `editPrefixLength` must no longer be applied — by run steps or labels.
|
||||
*/
|
||||
const editPrefixClearedRef = useRef(false);
|
||||
|
||||
/** Removes the pending chip once its steer is injected (the inline content
|
||||
* part becomes the durable record), and records the id so a 202 ACK that
|
||||
|
|
@ -625,7 +632,7 @@ export default function useResumableSSE(
|
|||
const setRunEnd = useSetRecoilState(store.runEndByIndex(runIndex));
|
||||
|
||||
const {
|
||||
stepHandler,
|
||||
stepHandler: rawStepHandler,
|
||||
finalHandler,
|
||||
errorHandler,
|
||||
clearStepMaps,
|
||||
|
|
@ -648,6 +655,24 @@ export default function useResumableSSE(
|
|||
setShowStopButton,
|
||||
});
|
||||
|
||||
/**
|
||||
* Run steps read the edit prefix from the submission, so once a SYNC has
|
||||
* replaced the response with the completion-local snapshot they must stop
|
||||
* offsetting — the prefix those indices were relative to is gone. Applied
|
||||
* here rather than at each dispatch so every path (live, replay, pending)
|
||||
* agrees, and so run steps and activity labels share one index space.
|
||||
*/
|
||||
const stepHandler = useCallback<typeof rawStepHandler>(
|
||||
(event, submission) =>
|
||||
rawStepHandler(
|
||||
event,
|
||||
editPrefixClearedRef.current && submission != null
|
||||
? ({ ...submission, editPrefixLength: 0 } as typeof submission)
|
||||
: submission,
|
||||
),
|
||||
[rawStepHandler],
|
||||
);
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const balanceQuery = useGetUserBalance({
|
||||
enabled: !!isAuthenticated && startupConfig?.balance?.enabled,
|
||||
|
|
@ -821,7 +846,7 @@ export default function useResumableSSE(
|
|||
* another part, and a gap fill would miss its own reservation and
|
||||
* leave the placeholder pending forever. */
|
||||
const prefixLength =
|
||||
currentSubmission.editedContent != null
|
||||
currentSubmission.editedContent != null && !editPrefixClearedRef.current
|
||||
? (currentSubmission.editPrefixLength ??
|
||||
(currentSubmission.initialResponse as TMessage | undefined)?.content?.length ??
|
||||
0)
|
||||
|
|
@ -1113,6 +1138,19 @@ export default function useResumableSSE(
|
|||
matchedByResponseId &&
|
||||
Array.isArray(oldContent) &&
|
||||
oldContent.length > 0;
|
||||
/**
|
||||
* Replacing the response with `aggregatedContent` drops the
|
||||
* prefix an edited resubmission had retained: the snapshot is
|
||||
* completion-local, indexed from zero. Every later event must
|
||||
* therefore stop offsetting, or it writes past the end of the
|
||||
* shorter array — for an activity label that means the fill
|
||||
* misses its own reservation and the placeholder never
|
||||
* resolves. Preserving `oldContent` keeps the prefix, and with
|
||||
* it the offset. Run steps and labels both read this.
|
||||
*/
|
||||
if (!preserveLoadedContent) {
|
||||
editPrefixClearedRef.current = true;
|
||||
}
|
||||
const responseMessage = {
|
||||
...messages[responseIdx],
|
||||
content: preserveLoadedContent ? oldContent : data.resumeState.aggregatedContent,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue