Fix pending user dedupe current-turn scope

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Frank Song
2026-06-26 17:57:17 +08:00
committed by nesquena-hermes
parent 540d36d0b0
commit 2d21eff780
2 changed files with 108 additions and 5 deletions
+20 -5
View File
@@ -7754,17 +7754,32 @@ async function _waitForServerThenReload(opts){
if(msgEl) msgEl.textContent='⚠️ Server is taking longer than expected — click Reload when ready';
}
function _pendingCurrentTailUserMessage(messages){
const list=Array.isArray(messages)?messages:[];
for(let i=list.length-1;i>=0;i--){
const msg=list[i];
if(!msg) continue;
if(String(msg.role||'')==='user') return msg;
if(msg._live||String(msg.role||'')==='tool') continue;
return null;
}
return null;
}
function getPendingSessionMessage(session, messagesOverride=null){
const text=String(session?.pending_user_message||'').trim();
if(!text) return null;
const attachments=Array.isArray(session?.pending_attachments)?session.pending_attachments.filter(Boolean):[];
const sourceMessages=Array.isArray(messagesOverride)?messagesOverride:session?.messages;
const messages=Array.isArray(sourceMessages)?sourceMessages:[];
const lastUser=[...messages].reverse().find(m=>m&&m.role==='user');
if(lastUser){
const lastText=String(msgContent(lastUser)||'').trim();
if(lastText===text){
if(attachments.length&&!lastUser.attachments?.length) lastUser.attachments=attachments;
const currentTailUser=_pendingCurrentTailUserMessage(messages);
if(currentTailUser){
const pendingCandidate={role:'user',content:text};
const sameCurrentTurn=typeof _sameTranscriptMessage==='function'
? _sameTranscriptMessage(currentTailUser,pendingCandidate)
: String(msgContent(currentTailUser)||'').trim()===text;
if(sameCurrentTurn){
if(attachments.length&&!currentTailUser.attachments?.length) currentTailUser.attachments=attachments;
return null;
}
}
+88
View File
@@ -143,6 +143,74 @@ process.stdout.write(JSON.stringify({{
return json.loads(proc.stdout)
def _run_pending_session_message_probe() -> dict:
prompt = "repeat me"
historical_workspace_prompt = f"[Workspace::v1: /tmp/old]\n{prompt}"
current_workspace_prompt = f"[Workspace::v1: /tmp/current]\n{prompt}"
helpers = "\n".join(
[
_function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"),
_function_body(UI_SRC, "function msgContent"),
_function_body(SESSIONS_SRC, "function _messageComparableText"),
_function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"),
_function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"),
_function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"),
_function_body(SESSIONS_SRC, "function _sameTranscriptMessage"),
_function_body(UI_SRC, "function _pendingCurrentTailUserMessage"),
_function_body(UI_SRC, "function getPendingSessionMessage"),
]
)
script = f"""
{helpers}
const prompt = {json.dumps(prompt)};
const historical = {{role:'user', content:prompt, _ts:1}};
const historicalWorkspace = {{role:'user', content:{json.dumps(historical_workspace_prompt)}, _ts:1}};
const historicalAnswer = {{role:'assistant', content:'done', _ts:2}};
const currentTail = {{role:'user', content:prompt, _ts:3}};
const currentWorkspaceTail = {{role:'user', content:{json.dumps(current_workspace_prompt)}, _ts:3}};
const liveAssistant = {{role:'assistant', content:'working', _live:true, _ts:4}};
const attachments = [{{name:'note.txt', path:'note.txt', mime:'text/plain'}}];
const fromHistoricalSameText = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:3}},
[historical, historicalAnswer]
);
const fromHistoricalWorkspace = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:3}},
[historicalWorkspace, historicalAnswer]
);
const exactCurrentMessages = [historical, historicalAnswer, currentTail];
const exactCurrentResult = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:4, pending_attachments:attachments}},
exactCurrentMessages
);
const workspaceCurrentResult = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:4}},
[historical, historicalAnswer, currentWorkspaceTail]
);
const liveAfterCurrentResult = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:4}},
[historical, historicalAnswer, currentWorkspaceTail, liveAssistant]
);
const differentTailResult = getPendingSessionMessage(
{{pending_user_message:prompt, pending_started_at:4}},
[historical, historicalAnswer, {{role:'user', content:'different prompt', _ts:3}}]
);
process.stdout.write(JSON.stringify({{
historicalSameTextSurvives: !!fromHistoricalSameText && fromHistoricalSameText.content===prompt && fromHistoricalSameText._pending===true,
historicalWorkspaceSurvives: !!fromHistoricalWorkspace && fromHistoricalWorkspace.content===prompt && fromHistoricalWorkspace._pending===true,
exactCurrentTailDedupe: exactCurrentResult===null,
exactCurrentTailAttachmentsCopied: Array.isArray(currentTail.attachments) && currentTail.attachments[0].name==='note.txt',
workspaceCurrentTailDedupe: workspaceCurrentResult===null,
liveAfterCurrentTailDedupe: liveAfterCurrentResult===null,
differentCurrentTailSurvives: !!differentTailResult && differentTailResult.content===prompt && differentTailResult._pending===true,
}}));
"""
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(proc.stdout)
def test_reattach_path_uses_replay_when_status_reports_journal():
reattach_pos = MESSAGES_SRC.index("let replayOnly=false;")
block = MESSAGES_SRC[reattach_pos : reattach_pos + 1200]
@@ -309,6 +377,26 @@ def test_user_turn_dedupe_is_scoped_to_current_turn_by_behavior():
assert result["inflightWithCurrentRoles"] == ["user", "assistant", "user", "assistant"]
def test_get_pending_session_message_keeps_deferred_repeat_prompt_by_behavior():
"""Deferred active reload must not hide a current repeat prompt.
In the default deferred save mode, chat start persists only
pending_user_message before the worker appends the display row. If an older
turn has the same visible user text, getPendingSessionMessage still has to
return the current pending row; downstream merge code then dedupes only
against the current tail.
"""
result = _run_pending_session_message_probe()
assert result["historicalSameTextSurvives"] is True
assert result["historicalWorkspaceSurvives"] is True
assert result["exactCurrentTailDedupe"] is True
assert result["exactCurrentTailAttachmentsCopied"] is True
assert result["workspaceCurrentTailDedupe"] is True
assert result["liveAfterCurrentTailDedupe"] is True
assert result["differentCurrentTailSurvives"] is True
def test_live_tool_matching_uses_the_same_aliases_as_live_card_dedup():
live_tid_pos = MESSAGES_SRC.index("function _liveToolTid")
live_tid_block = MESSAGES_SRC[live_tid_pos : live_tid_pos + 450]