fix(compressor): keep last visible assistant reply out of compaction summary + label handoffs in WebUI (#29824)
Two-pronged fix for the WebUI "context compaction block in place of last assistant response" regression. Agent layer (the real fix). ``_find_tail_cut_by_tokens`` already had ``_ensure_last_user_message_in_tail`` to keep the most recent user request out of the compressed middle (#10896), but no symmetric anchor for the assistant side. When the conversation has an oversized recent tool result or a long stretch of tool-call/result pairs *after* the assistant's last visible reply, the token-budget walk can stop with the previously-visible reply on the wrong side of ``cut_idx``. The summariser then rolls it into the single ``[CONTEXT COMPACTION — REFERENCE ONLY]`` block persisted as ``role="user"`` or ``role="assistant"``, and from the operator's perspective the WebUI session viewer (``web/src/pages/SessionsPage.tsx``) and the TUI chat panel both suddenly show the opaque "Context compaction" block in the slot where they were just reading the actual answer: User: "i cant see the output of the last message you sent, i did see it previously, however now see 'context compaction'" Added ``_ensure_last_assistant_message_in_tail`` mirror of the user-side anchor. It looks for the most recent assistant message with non-empty text content (skipping tool-call-only assistant "stubs" which the UI renders as small "calling tool X" indicators rather than a readable bubble) and walks ``cut_idx`` back through the standard ``_align_boundary_backward`` so we don't split a tool_call/result group that immediately precedes it. The two anchors are chained — each only walks ``cut_idx`` backward, so the tail can only grow. Falls back to "most recent assistant of any kind" only when no content-bearing reply exists in the compressible region (fresh multi-step tool sequence with no prior reply) — in that case the agent-side fix is effectively a no-op and the existing user-message anchor carries the load. WebUI layer (clarity). Added ``isCompactionMessage`` detector that recognises the ``[CONTEXT COMPACTION — REFERENCE ONLY]`` (current) and ``[CONTEXT SUMMARY]:`` (legacy) prefixes from ``agent/context_compressor.py``, and a new ``compaction`` entry in ``MessageBubble``'s ``ROLE_STYLES`` map. Compaction blocks now render as muted, italicised system-style rows labelled ``Context handoff`` — clearly metadata, not the assistant's actual reply — so an operator scrolling back through a long session can't mistake the summary for a real answer. Keeping the detected prefixes inline (rather than importing them) because the WebUI bundle has no Python interop. A guardrail comment points readers at the source-of-truth constants in ``agent/context_compressor.py``.
This commit is contained in:
parent
7a318aae22
commit
691ff7c188
2 changed files with 149 additions and 4 deletions
|
|
@ -147,6 +147,32 @@ function ToolCallBlock({
|
|||
);
|
||||
}
|
||||
|
||||
// Context-compaction handoff blocks are persisted as ``role="user"`` or
|
||||
// ``role="assistant"`` with content starting with one of these prefixes —
|
||||
// they're metadata inserted by ``agent/context_compressor.py``, NOT real
|
||||
// turns the user typed or the model replied with. Rendering them with
|
||||
// the same styling as regular messages confuses operators scrolling the
|
||||
// session timeline (#29824 — "WebUI can show context compaction block
|
||||
// instead of latest assistant response after compression"), so we
|
||||
// detect them here and downgrade them to a muted, clearly-labelled
|
||||
// "Context handoff" row.
|
||||
//
|
||||
// Keep these prefixes in sync with ``SUMMARY_PREFIX`` and
|
||||
// ``LEGACY_SUMMARY_PREFIX`` in ``agent/context_compressor.py``.
|
||||
const COMPACTION_PREFIXES = [
|
||||
"[CONTEXT COMPACTION — REFERENCE ONLY]",
|
||||
"[CONTEXT COMPACTION - REFERENCE ONLY]",
|
||||
"[CONTEXT SUMMARY]:",
|
||||
] as const;
|
||||
|
||||
function isCompactionMessage(msg: SessionMessage): boolean {
|
||||
if (msg.role !== "user" && msg.role !== "assistant") return false;
|
||||
const content = msg.content;
|
||||
if (typeof content !== "string") return false;
|
||||
const head = content.trimStart();
|
||||
return COMPACTION_PREFIXES.some((p) => head.startsWith(p));
|
||||
}
|
||||
|
||||
function MessageBubble({
|
||||
msg,
|
||||
highlight,
|
||||
|
|
@ -180,12 +206,25 @@ function MessageBubble({
|
|||
text: "text-warning",
|
||||
label: t.sessions.roles.tool,
|
||||
},
|
||||
// Compaction handoffs render as faded system-style metadata with a
|
||||
// distinctive label so they can't be mistaken for real assistant
|
||||
// replies during a scroll-back review (#29824).
|
||||
compaction: {
|
||||
bg: "bg-muted/50",
|
||||
text: "text-muted-foreground italic",
|
||||
label: "Context handoff",
|
||||
},
|
||||
};
|
||||
|
||||
const style = ROLE_STYLES[msg.role] ?? ROLE_STYLES.system;
|
||||
const label = msg.tool_name
|
||||
? `${t.sessions.roles.tool}: ${msg.tool_name}`
|
||||
: style.label;
|
||||
const isCompaction = isCompactionMessage(msg);
|
||||
const style = isCompaction
|
||||
? ROLE_STYLES.compaction
|
||||
: ROLE_STYLES[msg.role] ?? ROLE_STYLES.system;
|
||||
const label = isCompaction
|
||||
? ROLE_STYLES.compaction.label
|
||||
: msg.tool_name
|
||||
? `${t.sessions.roles.tool}: ${msg.tool_name}`
|
||||
: style.label;
|
||||
|
||||
// Check if any search term appears as a prefix of any word in content
|
||||
const isHit = (() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue