Stage 387: PR #2600

This commit is contained in:
nesquena-hermes
2026-05-19 22:10:20 +00:00
6 changed files with 161 additions and 4 deletions
+20
View File
@@ -53,6 +53,24 @@ def _content_has_part_type(content, part_types):
)
def _is_context_compression_marker(message):
"""Return true for synthetic compression/reference cards, not user turns."""
if not isinstance(message, dict):
return False
role = message.get("role")
if not role or role == "tool":
return False
text = _content_text(
message.get("content", ""),
part_types={"text", "input_text", "output_text"},
).lower().lstrip()
return (
text.startswith("[context compaction")
or text.startswith("context compaction")
or text.startswith("[your active task list was preserved across context compression]")
)
def visible_messages_for_anchor(messages, *, auto_compression: bool = False):
"""Return transcript messages that can anchor compression UI metadata.
@@ -70,6 +88,8 @@ def visible_messages_for_anchor(messages, *, auto_compression: bool = False):
role = message.get("role")
if not role or role == "tool":
continue
if _is_context_compression_marker(message):
continue
content = message.get("content", "")
has_attachments = bool(message.get("attachments"))
+17
View File
@@ -377,6 +377,11 @@ class Session:
compression_anchor_message_key=None,
compression_anchor_summary=None,
pre_compression_snapshot: bool=False,
context_engine=None,
compression_anchor_engine=None,
compression_anchor_mode=None,
compression_anchor_details=None,
context_engine_state=None,
context_length=None, threshold_tokens=None,
last_prompt_tokens=None,
gateway_routing=None, gateway_routing_history=None,
@@ -417,6 +422,11 @@ class Session:
self.compression_anchor_message_key = compression_anchor_message_key
self.compression_anchor_summary = compression_anchor_summary
self.pre_compression_snapshot = bool(pre_compression_snapshot)
self.context_engine = context_engine
self.compression_anchor_engine = compression_anchor_engine
self.compression_anchor_mode = compression_anchor_mode
self.compression_anchor_details = compression_anchor_details if isinstance(compression_anchor_details, dict) else {}
self.context_engine_state = context_engine_state if isinstance(context_engine_state, dict) else {}
self.context_length = context_length
self.threshold_tokens = threshold_tokens
self.last_prompt_tokens = last_prompt_tokens
@@ -481,6 +491,8 @@ class Session:
'pending_user_message', 'pending_attachments', 'pending_started_at',
'compression_anchor_visible_idx', 'compression_anchor_message_key',
'compression_anchor_summary', 'pre_compression_snapshot',
'context_engine', 'compression_anchor_engine', 'compression_anchor_mode',
'compression_anchor_details', 'context_engine_state',
'context_length', 'threshold_tokens', 'last_prompt_tokens',
'gateway_routing', 'gateway_routing_history', 'llm_title_generated',
'parent_session_id',
@@ -671,6 +683,11 @@ class Session:
'compression_anchor_message_key': self.compression_anchor_message_key,
'compression_anchor_summary': self.compression_anchor_summary,
'pre_compression_snapshot': self.pre_compression_snapshot,
'context_engine': self.context_engine,
'compression_anchor_engine': self.compression_anchor_engine,
'compression_anchor_mode': self.compression_anchor_mode,
'compression_anchor_details': self.compression_anchor_details,
'context_engine_state': self.context_engine_state,
'context_length': self.context_length,
'threshold_tokens': self.threshold_tokens,
'last_prompt_tokens': self.last_prompt_tokens,
+16
View File
@@ -196,6 +196,8 @@ const LOCALES = {
conversation_cleared: 'Conversation cleared',
command_label: 'Command',
context_compaction_label: 'Context compaction',
retrieval_context_label: 'Indexed context',
retrieval_context_preview: 'Earlier messages are stored and retrievable with context tools',
preserved_task_list_label: 'Preserved task list',
reference_only_label: 'Reference only',
model_usage: 'Usage: /model <name>',
@@ -1418,6 +1420,8 @@ const LOCALES = {
conversation_cleared: 'Conversazione cancellata',
command_label: 'Comando',
context_compaction_label: 'Compattazione contesto',
retrieval_context_label: 'Contesto indicizzato',
retrieval_context_preview: 'I messaggi precedenti sono archiviati e recuperabili con gli strumenti di contesto',
preserved_task_list_label: 'Lista task preservata',
reference_only_label: 'Solo riferimento',
model_usage: 'Uso: /model <nome>',
@@ -2632,6 +2636,8 @@ const LOCALES = {
conversation_cleared: '会話をクリアしました',
command_label: 'コマンド',
context_compaction_label: 'コンテキスト圧縮',
retrieval_context_label: 'インデックス済みコンテキスト',
retrieval_context_preview: '以前のメッセージは保存され、コンテキストツールで取得できます',
preserved_task_list_label: '保持されたタスクリスト',
reference_only_label: '参照専用',
model_usage: '使い方: /model <名前>',
@@ -3886,6 +3892,8 @@ const LOCALES = {
compress_failed_label: 'Ошибка сжатия',
compress_running_label: 'Сжатие…',
context_compaction_label: 'Сжатие контекста',
retrieval_context_label: 'Индексированный контекст',
retrieval_context_preview: 'Предыдущие сообщения сохранены и доступны через инструменты контекста',
preserved_task_list_label: 'Сохранённый список задач',
focus_label: 'Фокус',
model_search_no_results: 'Модели не найдены',
@@ -4996,6 +5004,8 @@ const LOCALES = {
conversation_cleared: 'Conversación borrada',
command_label: 'Comando',
context_compaction_label: 'Compacción de contexto',
retrieval_context_label: 'Contexto indexado',
retrieval_context_preview: 'Los mensajes anteriores se almacenan y se pueden recuperar con herramientas de contexto',
preserved_task_list_label: 'Lista de tareas conservada',
reference_only_label: 'Solo referencia',
model_usage: 'Uso: /model <name>',
@@ -6123,6 +6133,8 @@ const LOCALES = {
conversation_cleared: 'Konversation gelöscht',
command_label: 'Befehl',
context_compaction_label: 'Kontextkomprimierung',
retrieval_context_label: 'Indizierter Kontext',
retrieval_context_preview: 'Frühere Nachrichten sind gespeichert und über Kontextwerkzeuge abrufbar',
preserved_task_list_label: 'Beibehaltene Aufgabenliste',
reference_only_label: 'Nur Referenz',
model_usage: 'Nutzung: /model <name>',
@@ -7301,6 +7313,8 @@ const LOCALES = {
conversation_cleared: '对话已清空',
command_label: '命令',
context_compaction_label: '上下文压缩',
retrieval_context_label: '已索引上下文',
retrieval_context_preview: '较早消息已存储,可通过上下文工具检索',
preserved_task_list_label: '保留的任务列表',
reference_only_label: '仅供参考',
model_usage: '用法:/model <name>',
@@ -10724,6 +10738,8 @@ const LOCALES = {
conversation_cleared: '대화를 지웠습니다',
command_label: '명령',
context_compaction_label: 'Context compaction',
retrieval_context_label: 'Indexed context',
retrieval_context_preview: 'Earlier messages are stored and retrievable with context tools',
preserved_task_list_label: '보존된 작업 목록',
reference_only_label: 'Reference only',
model_usage: 'Usage: /model <name>',
+3
View File
@@ -1849,6 +1849,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
phase:'done',
automatic:true,
message,
engine:d.engine,
mode:d.mode,
details:d.details,
summary:{headline:message},
continuationSessionId:continuationSid,
};
+33 -4
View File
@@ -5096,9 +5096,10 @@ function _autoCompressionBaseDetail(state){
: (String(state&&state.message||fallback).trim()||fallback);
}
function _autoCompressionPreviewText(state){
const copy=_engineAwareCompressionCopy(String(state&&state.engine||_compressionEngineForSession()).toLowerCase(), String(state&&state.mode||_compressionModeForSession()).toLowerCase());
const running=state&&state.phase==='running';
const detail=_autoCompressionBaseDetail(state);
if(!running) return (String(state&&state.summary?.headline||detail).trim()||detail);
if(!running) return (String(state&&state.summary?.headline||copy.preview||detail).trim()||detail);
const elapsedLabel=_compressionElapsedLabel(state);
return [detail, elapsedLabel].filter(Boolean).join(' · ');
}
@@ -5112,13 +5113,14 @@ function _autoCompressionDetailText(state){
return [base,handoff].filter(Boolean).join('\n');
}
function _autoCompressionCardsHtml(state){
const copy=_engineAwareCompressionCopy(String(state&&state.engine||_compressionEngineForSession()).toLowerCase(), String(state&&state.mode||_compressionModeForSession()).toLowerCase());
const running=state&&state.phase==='running';
const preview=_autoCompressionPreviewText(state);
const cardDetail=_autoCompressionDetailText(state);
return `
<div class="tool-card-row compression-card-row" data-compression-card="1">
${_compressionStatusCardHtml({
statusLabel: t('auto_compress_label'),
statusLabel: (String(state&&state.engine||'').toLowerCase()==='lcm'||String(state&&state.mode||'').toLowerCase()==='lossless_retrieval')?copy.label:t('auto_compress_label'),
previewText: preview,
detail: cardDetail,
icon: running ? '<span class="tool-card-running-dot"></span>' : li('check',13),
@@ -5286,14 +5288,15 @@ function _latestCompressionReferenceMessage(messages, summaryText=''){
return {message:null, rawIdx:-1};
}
function _compressionReferenceCardHtml(text, open=false){
const copy=_engineAwareCompressionCopy();
const preview=text.split(/\n+/).filter(Boolean).slice(0,2).join(' ');
return `
<div class="tool-card-row compression-card-row" data-compression-card="1" data-raw-text="${esc(text)}">
<div class="tool-card tool-card-compress-reference${open?' open':''}">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
<span class="tool-card-icon">${li('star',13)}</span>
<span class="tool-card-name">${esc(t('context_compaction_label'))}</span>
<span class="tool-card-preview">${esc(t('reference_only_label'))} · ${esc(preview)}</span>
<span class="tool-card-name">${esc(copy.label)}</span>
<span class="tool-card-preview">${esc(copy.preview)} · ${esc(preview)}</span>
<span class="tool-card-toggle">${li('chevron-right',12)}</span>
<button class="msg-copy-btn msg-action-btn tool-card-copy compression-reference-copy" title="${t('copy')}" onclick="copyMsg(this);event.stopPropagation()">${li('copy',13)}</button>
</div>
@@ -5367,6 +5370,31 @@ function _formatMessageFooterTimestamp(tsVal){
const opts={month:'short', day:'numeric', hour:'numeric', minute:'2-digit'};
return fmt?fmt(date,opts):date.toLocaleString([], opts);
}
function _compressionEngineForSession(){
return String(
(S.session&&(
S.session.compression_anchor_engine
|| S.session.context_engine
)) || 'compressor'
).trim().toLowerCase() || 'compressor';
}
function _compressionModeForSession(){
return String(
(S.session&&S.session.compression_anchor_mode) || 'summary_compaction'
).trim().toLowerCase() || 'summary_compaction';
}
function _engineAwareCompressionCopy(engine=_compressionEngineForSession(), mode=_compressionModeForSession()){
if(engine==='lcm'||mode==='lossless_retrieval'){
return {
label:t('retrieval_context_label'),
preview:t('retrieval_context_preview'),
};
}
return {
label:t('context_compaction_label'),
preview:t('reference_only_label'),
};
}
function _compressionStatusCardHtml({
statusLabel,
previewText,
@@ -5946,6 +5974,7 @@ function renderMessages(options){
}
function _insertCompressionLikeNodeByRawIdx(node, rawIdx){
if(!node) return;
if(rawIdx<firstRenderedRawIdx) return;
if(!renderVisWithIdx.length){
inner.appendChild(node);
return;
+72
View File
@@ -1,5 +1,7 @@
from pathlib import Path
from api.compression_anchor import visible_messages_for_anchor
from api.models import Session
from api.streaming import _is_fallback_lifecycle_message
@@ -475,6 +477,76 @@ def test_reference_message_inserted_before_future_assistant_anchor():
assert helper.index("blocks.insertBefore(node, anchorSeg);") < helper.index("const userRow=userRows.get(anchorRawIdx);")
def test_frontend_uses_context_engine_metadata_for_indexed_context_copy():
src = _read("static/ui.js")
i18n = _read("static/i18n.js")
assert "function _compressionEngineForSession" in src
assert "S.session.compression_anchor_engine" in src
assert "S.session.context_engine" in src
assert "function _compressionModeForSession" in src
assert "S.session.compression_anchor_mode" in src
assert "function _engineAwareCompressionCopy" in src
assert "mode==='lossless_retrieval'" in src
assert "t('retrieval_context_label')" in src
assert "t('retrieval_context_preview')" in src
assert "retrieval_context_label" in i18n
assert "retrieval_context_preview" in i18n
def test_session_model_round_trips_context_engine_metadata(tmp_path, monkeypatch):
import api.models as models
state_dir = tmp_path / "state"
session_dir = state_dir / "sessions"
session_dir.mkdir(parents=True)
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", state_dir / "session_index.json")
session = Session(
session_id="lcm_metadata",
workspace=str(tmp_path),
context_engine="lcm",
compression_anchor_engine="lcm",
compression_anchor_mode="lossless_retrieval",
compression_anchor_details={"retrieval_tools": ["lcm_grep"]},
context_engine_state={"status": "indexed"},
)
session.save(touch_updated_at=False)
loaded = Session.load("lcm_metadata")
assert loaded.context_engine == "lcm"
assert loaded.compression_anchor_engine == "lcm"
assert loaded.compression_anchor_mode == "lossless_retrieval"
assert loaded.compression_anchor_details == {"retrieval_tools": ["lcm_grep"]}
assert loaded.context_engine_state == {"status": "indexed"}
def test_backend_auto_anchor_count_excludes_compaction_marker_cards():
messages = [
{"role": "user", "content": "before compression"},
{"role": "assistant", "content": "[CONTEXT COMPACTION — REFERENCE ONLY] summary"},
{"role": "assistant", "content": "after compression"},
{"role": "tool", "content": "hidden tool output"},
{"role": "user", "content": "[Your active task list was preserved across context compression]"},
]
visible = visible_messages_for_anchor(messages, auto_compression=True)
assert [m["content"] for m in visible] == ["before compression", "after compression"]
def test_frontend_reference_insertion_skips_when_reference_is_before_render_window():
src = _read("static/ui.js")
start = src.find("function _insertCompressionLikeNodeByRawIdx")
assert start != -1, "raw-index insertion helper not found"
end = src.find("const preservedOnlyNode=", start)
assert end != -1, "raw-index insertion helper end not found"
helper = src[start:end]
assert "if(rawIdx<firstRenderedRawIdx) return;" in helper
def test_reference_message_selection_prefers_latest_matching_marker():
src = _read("static/ui.js")
start = src.find("function _latestCompressionReferenceMessage")