Skip to content

Commit 9a1acbf

Browse files
nesquena-hermeshermes-agentnesquena-hermes
authored
Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920) (#5952)
* fix: dedupe pending prompt across compaction * Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920, starship-s) --------- Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com> Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
1 parent 20d258d commit 9a1acbf

4 files changed

Lines changed: 106 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
### Fixed
77

8+
- **A pending prompt no longer renders twice across a context-compaction boundary.** On reload / reattach to an active turn, a synthetic `[CONTEXT COMPACTION]` marker (a user-role row that isn't a real submitted turn) placed after your prompt was treated as the latest user message, so the tail scan missed the actual prompt right before it and rendered the same pending prompt twice. The session-load and refresh/reconnect tail scans now skip compaction markers when locating the current user message, while completed assistant rows stay hard boundaries so genuinely-repeated prompts still render. Thanks @starship-s. (#5920)
9+
810
- **Sessions from Zed and other ACP (Agent Client Protocol) clients now show up in the sidebar.** An ACP-adapter session persists to the agent database with source `acp`, which was classified as `other` and fell through both sidebar buckets — so those conversations were completely invisible and unclickable. ACP sessions are now grouped with the CLI/TUI family (labelled "ACP"), with the same zero-message / ended-connection hiding rules as other interactive agent sessions. Thanks @ai-ag2026. (#5939)
911

1012
- **Date-stamped legacy Claude 3.0 model ids no longer show reasoning-effort controls they don't support.** The capability check read an 8-digit date suffix (e.g. `claude-3-opus-20240229`) as the model's minor version, so a bare date-stamped Claude 3.0 id wrongly qualified as reasoning-capable. The minor-version match is now bounded (mirroring the existing date-stamp defense), so date-stamped Claude 3.0 ids correctly hide reasoning-effort options while Claude 3.7 / 4.x / adaptive keep them. Thanks @nankingjing. (#5934)

static/sessions.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3013,7 +3013,11 @@ function _currentTailUserMessage(messages){
30133013
for(let i=list.length-1;i>=0;i--){
30143014
const msg=list[i];
30153015
if(!msg) continue;
3016-
if(String(msg.role||'')==='user') return msg;
3016+
if(String(msg.role||'')==='user'){
3017+
// Compaction rows are synthetic user-role markers, not submitted turns.
3018+
if(typeof _isContextCompactionMessage==='function'&&_isContextCompactionMessage(msg)) continue;
3019+
return msg;
3020+
}
30173021
if(msg._live||String(msg.role||'')==='tool') continue;
30183022
return null;
30193023
}

static/ui.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9845,7 +9845,11 @@ function _pendingCurrentTailUserMessage(messages){
98459845
for(let i=list.length-1;i>=0;i--){
98469846
const msg=list[i];
98479847
if(!msg) continue;
9848-
if(String(msg.role||'')==='user') return msg;
9848+
if(String(msg.role||'')==='user'){
9849+
// Compaction rows are synthetic user-role markers, not submitted turns.
9850+
if(typeof _isContextCompactionMessage==='function'&&_isContextCompactionMessage(msg)) continue;
9851+
return msg;
9852+
}
98499853
if(msg._live||String(msg.role||'')==='tool') continue;
98509854
return null;
98519855
}

tests/test_run_journal_frontend_static.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def _run_current_turn_scope_probe() -> dict:
8080
helpers = "\n".join(
8181
[
8282
_function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"),
83+
_function_body(UI_SRC, "function msgContent"),
84+
_function_body(UI_SRC, "function _isContextCompactionText"),
85+
_function_body(UI_SRC, "function _isContextCompactionMessage"),
8386
_function_body(SESSIONS_SRC, "function _messageComparableText"),
8487
_function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"),
8588
_function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"),
@@ -128,6 +131,35 @@ def _run_current_turn_scope_probe() -> dict:
128131
[{{role:'user', content:{json.dumps(prompt)}, _ts:3}}, liveAssistant]
129132
);
130133
134+
const compaction = {{
135+
role:'user',
136+
content:'[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.',
137+
_ts:3.5,
138+
}};
139+
const compactionBase = [historical, historicalAnswer, optimisticCurrent, compaction];
140+
const compactionCandidate = {{role:'user', content:{json.dumps(prompt)}, _ts:3}};
141+
const compactionCurrentTail = _currentTailUserMessage(compactionBase);
142+
const compactionTailDuplicate = _hasCurrentTailUserDuplicate(compactionBase, compactionCandidate);
143+
const compactionMerged = _mergeInflightTailMessages(
144+
compactionBase,
145+
[compactionCandidate, liveAssistant]
146+
);
147+
const insertedAfterCompaction = _mergePendingSessionMessage(pendingSession, compactionMerged);
148+
const compactionPromptCount = compactionMerged.filter(
149+
m=>m&&m.role==='user'&&m._ts===3&&_normalizeUserTranscriptText(m.content)==={json.dumps(prompt)}
150+
).length;
151+
const compactionMarkerRetained = compactionMerged.some(m=>_isContextCompactionMessage(m));
152+
const compactionLiveAssistantRetained = compactionMerged.some(
153+
m=>m&&m.role==='assistant'&&m._live&&m.content==='working'
154+
);
155+
const completedBoundaryDedupe = _hasCurrentTailUserDuplicate(
156+
[historical, historicalAnswer, compaction],
157+
{{role:'user', content:{json.dumps(prompt)}, _ts:3}}
158+
);
159+
const distinctCompletedTurnPromptCount = inflightAfterHistory.filter(
160+
m=>m&&m.role==='user'&&_normalizeUserTranscriptText(m.content)==={json.dumps(prompt)}
161+
).length;
162+
131163
process.stdout.write(JSON.stringify({{
132164
insertedAfterHistory,
133165
pendingAfterHistoryRoles: pendingAfterHistory.map(m=>m.role),
@@ -137,6 +169,14 @@ def _run_current_turn_scope_probe() -> dict:
137169
pendingWithCurrentRoles: pendingWithCurrent.map(m=>m.role),
138170
inflightAfterHistoryRoles: inflightAfterHistory.map(m=>m.role),
139171
inflightWithCurrentRoles: inflightWithCurrent.map(m=>m.role),
172+
insertedAfterCompaction,
173+
compactionCurrentTailContent: compactionCurrentTail&&compactionCurrentTail.content,
174+
compactionTailDuplicate,
175+
compactionPromptCount,
176+
compactionMarkerRetained,
177+
compactionLiveAssistantRetained,
178+
completedBoundaryDedupe,
179+
distinctCompletedTurnPromptCount,
140180
}}));
141181
"""
142182
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
@@ -157,6 +197,8 @@ def _run_pending_session_message_probe() -> dict:
157197
_function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"),
158198
_function_body(SESSIONS_SRC, "function _sameTranscriptMessage"),
159199
_function_body(UI_SRC, "function _pendingCurrentTailUserMessage"),
200+
_function_body(UI_SRC, "function _isContextCompactionText"),
201+
_function_body(UI_SRC, "function _isContextCompactionMessage"),
160202
_function_body(UI_SRC, "function getPendingSessionMessage"),
161203
]
162204
)
@@ -168,8 +210,24 @@ def _run_pending_session_message_probe() -> dict:
168210
const historicalAnswer = {{role:'assistant', content:'done', _ts:2}};
169211
const currentTail = {{role:'user', content:prompt, _ts:3}};
170212
const currentWorkspaceTail = {{role:'user', content:{json.dumps(current_workspace_prompt)}, _ts:3}};
213+
const currentTailForCompaction = {{role:'user', content:prompt, _ts:3}};
171214
const liveAssistant = {{role:'assistant', content:'working', _live:true, _ts:4}};
172215
const attachments = [{{name:'note.txt', path:'note.txt', mime:'text/plain'}}];
216+
const compactionMarker = {{
217+
role:'user',
218+
content:'[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.',
219+
_ts:3.5,
220+
}};
221+
const repeatedPromptTurnOne = {{role:'user', content:prompt, _ts:1}};
222+
const repeatedPromptAnswerOne = {{role:'assistant', content:'done', _ts:2}};
223+
const repeatedPromptTurnTwo = {{role:'user', content:prompt, _ts:3}};
224+
const repeatedPromptAnswerTwo = {{role:'assistant', content:'done', _ts:4}};
225+
const repeatedCompletedBase = [
226+
repeatedPromptTurnOne,
227+
repeatedPromptAnswerOne,
228+
repeatedPromptTurnTwo,
229+
repeatedPromptAnswerTwo,
230+
];
173231
174232
const fromHistoricalSameText = getPendingSessionMessage(
175233
{{pending_user_message:prompt, pending_started_at:3}},
@@ -196,6 +254,21 @@ def _run_pending_session_message_probe() -> dict:
196254
{{pending_user_message:prompt, pending_started_at:4}},
197255
[historical, historicalAnswer, {{role:'user', content:'different prompt', _ts:3}}]
198256
);
257+
const compactionTailResult = getPendingSessionMessage(
258+
{{pending_user_message:prompt, pending_started_at:4, pending_attachments:attachments}},
259+
[historical, historicalAnswer, currentTailForCompaction, compactionMarker]
260+
);
261+
const repeatedCompletedResult = getPendingSessionMessage(
262+
{{pending_user_message:prompt, pending_started_at:5}},
263+
repeatedCompletedBase
264+
);
265+
const repeatedCompletedMessages = repeatedCompletedResult
266+
? [...repeatedCompletedBase, repeatedCompletedResult]
267+
: repeatedCompletedBase;
268+
const repeatedCompletedPromptCount = repeatedCompletedMessages.filter(
269+
m=>m&&m.role==='user'&&_normalizeUserTranscriptText(m.content)===prompt
270+
).length;
271+
const compactionCurrentTail = _pendingCurrentTailUserMessage([historical, historicalAnswer, currentTailForCompaction, compactionMarker]);
199272
200273
process.stdout.write(JSON.stringify({{
201274
historicalSameTextSurvives: !!fromHistoricalSameText && fromHistoricalSameText.content===prompt && fromHistoricalSameText._pending===true,
@@ -205,6 +278,12 @@ def _run_pending_session_message_probe() -> dict:
205278
workspaceCurrentTailDedupe: workspaceCurrentResult===null,
206279
liveAfterCurrentTailDedupe: liveAfterCurrentResult===null,
207280
differentCurrentTailSurvives: !!differentTailResult && differentTailResult.content===prompt && differentTailResult._pending===true,
281+
compactionBoundaryDedupe: compactionTailResult===null,
282+
compactionBoundaryCurrentTail: compactionCurrentTail&&compactionCurrentTail.role==='user'&&compactionCurrentTail.content===prompt,
283+
compactionCurrentTailAttachmentsCopied: Array.isArray(currentTailForCompaction.attachments) && currentTailForCompaction.attachments[0].name==='note.txt',
284+
repeatedCompletedPromptsRemainValid: repeatedCompletedPromptCount===3,
285+
isContextCompactionText: _isContextCompactionText(compactionMarker.content),
286+
isContextCompactionMessage: _isContextCompactionMessage(compactionMarker),
208287
}}));
209288
"""
210289
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
@@ -385,6 +464,15 @@ def test_user_turn_dedupe_is_scoped_to_current_turn_by_behavior():
385464
assert result["inflightAfterHistoryRoles"] == ["user", "assistant", "user", "assistant"]
386465
assert result["inflightWithCurrentRoles"] == ["user", "assistant", "user", "assistant"]
387466

467+
assert result["insertedAfterCompaction"] is False
468+
assert result["compactionCurrentTailContent"] == "[Workspace::v1: /tmp/current]\nrepeat me"
469+
assert result["compactionTailDuplicate"] is True
470+
assert result["compactionPromptCount"] == 1
471+
assert result["compactionMarkerRetained"] is True
472+
assert result["compactionLiveAssistantRetained"] is True
473+
assert result["completedBoundaryDedupe"] is False
474+
assert result["distinctCompletedTurnPromptCount"] == 2
475+
388476

389477
def test_get_pending_session_message_keeps_deferred_repeat_prompt_by_behavior():
390478
"""Deferred active reload must not hide a current repeat prompt.
@@ -404,6 +492,12 @@ def test_get_pending_session_message_keeps_deferred_repeat_prompt_by_behavior():
404492
assert result["workspaceCurrentTailDedupe"] is True
405493
assert result["liveAfterCurrentTailDedupe"] is True
406494
assert result["differentCurrentTailSurvives"] is True
495+
assert result["compactionBoundaryDedupe"] is True
496+
assert result["compactionBoundaryCurrentTail"] is True
497+
assert result["compactionCurrentTailAttachmentsCopied"] is True
498+
assert result["repeatedCompletedPromptsRemainValid"] is True
499+
assert result["isContextCompactionText"] is True
500+
assert result["isContextCompactionMessage"] is True
407501

408502

409503
def test_live_tool_matching_uses_the_same_aliases_as_live_card_dedup():

0 commit comments

Comments
 (0)