fix(send): snapshot files + clear draft pre-await (gate fixes)
#5912 gate found 2 defects the capture-clear salvage exposed: - [CORE] uploadPendingFiles() read still-live S.pendingFiles, so a re-entrant/ interrupt send during the upload window inherited + re-uploaded the first send's attachment. Now snapshot _submittedFiles before the await, detach from S.pendingFiles, and upload exactly that snapshot. - [SILENT] the persisted-draft clear ran AFTER the await, cancelling a draft typed during the upload window. Moved it (and _composerDraftClearPromise) alongside the textarea clear, before any await. Failed-send restore still chains off the promise via the pre-await _failedSendFilesSnapshot. Updated source-assertion tests for the new call shape + ordering.
This commit is contained in:
1 parent
67c9277ed4
commit
d6323bfff0
3 files changed
+38
-21
No files matched your search
+25
-13
@@ -1566,9 +1566,27 @@ async function send(){
|
||||
const _submittedDraftTextForClear=$('msg').value||'';
|
||||
$('msg').value='';autoResize();
|
||||
|
||||
setComposerStatus(S.pendingFiles&&S.pendingFiles.length?'Uploading…':'');
|
||||
// #5912 gate CORE fix: snapshot the pending files that belong to THIS send
|
||||
// BEFORE the await, and upload exactly that snapshot. Otherwise a re-entrant /
|
||||
// interrupt-mode send during the upload window inherits the still-live
|
||||
// S.pendingFiles and later re-uploads the first send's attachment. Detach the
|
||||
// snapshot from S.pendingFiles now so files staged AFTER this point belong to
|
||||
// the next send only.
|
||||
const _submittedFiles=[...(S.pendingFiles||[])];
|
||||
const _submittedDraftFilesForClear=_submittedFiles.map(f=>(f&&f.name)||'').filter(Boolean);
|
||||
S.pendingFiles=[];
|
||||
if(typeof renderTray==='function')renderTray();
|
||||
|
||||
// #5912 gate SILENT fix: clear the PERSISTED draft here — alongside the
|
||||
// textarea clear, BEFORE any await — so a new draft typed during the upload
|
||||
// window is not clobbered by a delayed text:'' post. Keep the promise so the
|
||||
// #5472 failed-send restore can chain its re-persist after this clear resolves.
|
||||
let _composerDraftClearPromise=null;
|
||||
if (activeSid && typeof _clearComposerDraft === 'function') _composerDraftClearPromise=_clearComposerDraft(activeSid,_submittedDraftTextForClear,_submittedDraftFilesForClear);
|
||||
|
||||
setComposerStatus(_submittedFiles.length?'Uploading…':'');
|
||||
let uploaded=[];
|
||||
try{uploaded=await uploadPendingFiles();}
|
||||
try{uploaded=await uploadPendingFiles({files:_submittedFiles, sessionId:activeSid});}
|
||||
catch(e){if(!text){setComposerStatus(`Upload error: ${e.message}`);return;}}
|
||||
// Clear the uploading status now that upload is done — if we don't clear here
|
||||
// it stays visible for the entire duration of the agent stream, since
|
||||
@@ -1603,17 +1621,11 @@ async function send(){
|
||||
}
|
||||
}
|
||||
if(!msgText){setComposerStatus('Nothing to send');return;}
|
||||
// Composer textarea + _submittedDraftTextForClear were already captured and
|
||||
// cleared immediately after capture (above, salvage of #4750) to close the
|
||||
// re-entrant double-send race. Only the files snapshot is finalized here.
|
||||
const _submittedDraftFilesForClear=Array.isArray(_failedSendFilesSnapshot)?[..._failedSendFilesSnapshot]:[];
|
||||
// Clear persisted composer draft since message was sent. Capture the promise
|
||||
// so the #5472 failed-send restore can chain its re-persist AFTER this clear
|
||||
// resolves — otherwise the two same-origin POSTs (clear text:'' then restore
|
||||
// text:<draft>) can be reordered under HTTP/2 multiplexing and leave the
|
||||
// server draft empty after a reload. (Opus #5484 NIT.)
|
||||
let _composerDraftClearPromise=null;
|
||||
if (activeSid && typeof _clearComposerDraft === 'function') _composerDraftClearPromise=_clearComposerDraft(activeSid,_submittedDraftTextForClear,_submittedDraftFilesForClear);
|
||||
// Composer textarea + persisted draft were already captured and cleared
|
||||
// immediately after capture (above, salvage of #4750 + #5912 gate fix) to close
|
||||
// the re-entrant double-send race AND avoid clobbering a draft typed during the
|
||||
// upload window. _composerDraftClearPromise / _submittedDraftFilesForClear are
|
||||
// set there; nothing to re-declare here.
|
||||
const displayText=_slashDisplayTextOverride||text||(uploaded.length?`Uploaded: ${uploadedNames.join(', ')}`:'(file upload)');
|
||||
const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploadedNames:undefined,_ts:Date.now()/1000};
|
||||
S.toolCalls=[]; // clear tool calls from previous turn
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_composer_captured_and_cleared_before_upload_await():
|
||||
wipe_rel = body.index("$('msg').value='';autoResize();", capture_idx)
|
||||
assert wipe_rel - capture_idx < 120, "the textarea wipe must sit immediately after the capture"
|
||||
|
||||
upload_idx = body.index("uploaded=await uploadPendingFiles();")
|
||||
upload_idx = body.index("uploaded=await uploadPendingFiles(")
|
||||
directive_await_idx = body.index("const _directivePayload = await _pending.promise;")
|
||||
|
||||
assert capture_idx < upload_idx, (
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_send_captures_immutable_snapshot_before_rewrites_and_upload():
|
||||
"const _failedSendFilesSnapshot=Array.isArray(S.pendingFiles)?[...S.pendingFiles]:[];"
|
||||
)
|
||||
moa_idx = MESSAGES_JS.find("text=_moaArgs;")
|
||||
upload_idx = MESSAGES_JS.find("uploaded=await uploadPendingFiles();")
|
||||
upload_idx = MESSAGES_JS.find("uploaded=await uploadPendingFiles(")
|
||||
assert snap_idx != -1 and files_idx != -1, "send() must snapshot text + files for #5472"
|
||||
assert moa_idx != -1 and upload_idx != -1
|
||||
# Snapshot happens before both the /moa rewrite and the upload drain.
|
||||
@@ -116,7 +116,7 @@ def test_send_still_clears_composer_on_the_happy_path():
|
||||
assert "$('msg').value='';autoResize();" in window_after_capture, (
|
||||
"the composer textarea wipe must sit immediately after the capture"
|
||||
)
|
||||
upload_idx = MESSAGES_JS.index("uploaded=await uploadPendingFiles();")
|
||||
upload_idx = MESSAGES_JS.index("uploaded=await uploadPendingFiles(")
|
||||
clear_idx = MESSAGES_JS.index(main_clear)
|
||||
# THE FIX: capture+wipe happen before the upload await (closes the race)...
|
||||
assert capture_idx < upload_idx, (
|
||||
@@ -127,12 +127,17 @@ def test_send_still_clears_composer_on_the_happy_path():
|
||||
assert capture_idx < clear_idx, (
|
||||
"the captured send-time draft text must precede the persisted-draft clear"
|
||||
)
|
||||
# The files snapshot is finalized just above the persisted-draft clear.
|
||||
window_before_clear = MESSAGES_JS[clear_idx - 700:clear_idx]
|
||||
# The files snapshot (#5912 gate fix) is taken from S.pendingFiles BEFORE the
|
||||
# upload await and feeds the persisted-draft clear which now runs before the await.
|
||||
assert (
|
||||
"const _submittedDraftFilesForClear=Array.isArray(_failedSendFilesSnapshot)?[..._failedSendFilesSnapshot]:[];"
|
||||
in window_before_clear
|
||||
), "the files snapshot for the persisted-draft clear must precede it"
|
||||
"const _submittedDraftFilesForClear=_submittedFiles.map(f=>(f&&f.name)||'').filter(Boolean);"
|
||||
in MESSAGES_JS
|
||||
), "the files snapshot for the persisted-draft clear must be captured pre-await"
|
||||
# The persisted-draft clear must run BEFORE the upload await (not after it),
|
||||
# so a draft typed during the upload window is not clobbered.
|
||||
assert clear_idx < upload_idx, (
|
||||
"the persisted-draft clear must precede the uploadPendingFiles() await (#5912)"
|
||||
)
|
||||
|
||||
|
||||
def test_restore_persist_chains_after_the_clear_promise():
|
||||
|
||||
Reference in new issue
Block a user