fix(office-ui): stop progress-row flicker and surface native role replies end to end
Two user-visible defects in company mode, root-caused via project 6666/8888 DB forensics: Progress-row flicker (thinking preview appearing/disappearing): progress entries were broadcast to clients before reaching the persistence buffer, so a tool_call-triggered session_detail snapshot rebuilt from the DB erased freshly streamed entries from the live log. Buffer now fills before the broadcast and session_detail flushes it before reading. Native role transcripts incomplete (thinking only at start, no narration, no final summary — external agents unaffected): - thinking deltas shared one stream id per conversation turn while seq reset per iteration, collapsing all iterations into one entry and silently dropping live thinking from iteration 2 on; now keyed per iteration like assistant deltas - assistant_delta events were mapped to None; company mode now surfaces them as streaming 'assistant' progress entries (rendered as Reply cards, merged like thinking, excluded from inline chat rows) - thinking was persisted one row per token, flooding the 1000-entry cap and evicting interleaved tool history; append_progress now folds streaming deltas per (type, turn, stream) with seq dedup - the terminal company turn was hidden at summary detail and, worse, its id-keyed backfill merge kept the first-inserted intermediate content, so the final reply never reached any channel; terminal turns are now flagged company_final_turn, visible at summary detail, and carry their own ui_message_id so they insert as fresh rows - appendProgressEntry applied its seq guard against unrelated entries when the stream key was absent from the log, killing the first delta of any fresh stream; the guard now only applies within the same stream Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1599,6 +1599,58 @@ class ChatStore:
|
||||
# concern, swap this in-row JSON blob for a proper rolling table.
|
||||
_PROGRESS_MAX_ENTRIES = 1000
|
||||
|
||||
# Streaming text types arrive as one entry per token-sized delta. Without
|
||||
# folding, thinking floods the entry cap (forensics: 929/1000 entries were
|
||||
# single-token thinking rows) and evicts the interleaved tool history.
|
||||
_PROGRESS_STREAM_MERGE_TYPES = frozenset({"thinking", "assistant"})
|
||||
|
||||
@staticmethod
|
||||
def _progress_stream_key(entry: dict[str, Any]) -> tuple[str, str, str] | None:
|
||||
entry_type = str(entry.get("type", "") or "")
|
||||
if entry_type not in ChatStore._PROGRESS_STREAM_MERGE_TYPES:
|
||||
return None
|
||||
item_id = str(entry.get("item_id") or entry.get("stream_id") or "").strip()
|
||||
if not item_id:
|
||||
return None
|
||||
return (entry_type, str(entry.get("turn_id", "") or ""), item_id)
|
||||
|
||||
@classmethod
|
||||
def _fold_progress_entries(
|
||||
cls,
|
||||
existing: list[dict[str, Any]],
|
||||
new_entries: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Fold streaming deltas into their stream's entry (mirrors the
|
||||
frontend ``appendProgressEntry`` merge so persisted state equals what
|
||||
the live client built)."""
|
||||
merged = list(existing)
|
||||
index_by_key: dict[tuple[str, str, str], int] = {}
|
||||
for i, entry in enumerate(merged):
|
||||
key = cls._progress_stream_key(entry)
|
||||
if key is not None:
|
||||
index_by_key[key] = i
|
||||
for entry in new_entries:
|
||||
key = cls._progress_stream_key(entry)
|
||||
if key is None or key not in index_by_key:
|
||||
if key is not None:
|
||||
index_by_key[key] = len(merged)
|
||||
merged.append(entry)
|
||||
continue
|
||||
target = merged[index_by_key[key]]
|
||||
last_seq = target.get("seq")
|
||||
new_seq = entry.get("seq")
|
||||
if isinstance(last_seq, (int, float)) and isinstance(new_seq, (int, float)) and new_seq <= last_seq:
|
||||
continue
|
||||
# Deltas are disjoint token fragments — concatenate raw, no strip.
|
||||
detail = f"{target.get('detail') or ''}{entry.get('detail') or ''}"
|
||||
preview = " ".join(detail.split())
|
||||
folded = dict(target)
|
||||
folded.update(entry)
|
||||
folded["detail"] = detail
|
||||
folded["summary"] = preview[:120].rstrip() + ("..." if len(preview) > 120 else "")
|
||||
merged[index_by_key[key]] = folded
|
||||
return merged
|
||||
|
||||
async def append_progress(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -1611,7 +1663,7 @@ class ChatStore:
|
||||
the first call creates the row and subsequent calls update it.
|
||||
"""
|
||||
existing = await self.get_progress(task_id, project_id=project_id)
|
||||
merged = (existing + new_entries)[-self._PROGRESS_MAX_ENTRIES:]
|
||||
merged = self._fold_progress_entries(existing, new_entries)[-self._PROGRESS_MAX_ENTRIES:]
|
||||
|
||||
async def _write() -> None:
|
||||
await self._db.execute(
|
||||
|
||||
Reference in New Issue
Block a user