From 6719d3f3f0ff8c7cc58a0298a4017954abf3ae4c Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 13 Mar 2026 15:40:47 -0600 Subject: [PATCH] fix: render collapsed turn summaries in outbound context Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/mnemosyne/blocks.py | 140 ++++++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 41 deletions(-) diff --git a/src/mnemosyne/blocks.py b/src/mnemosyne/blocks.py index 05ec772..747b15e 100644 --- a/src/mnemosyne/blocks.py +++ b/src/mnemosyne/blocks.py @@ -20,15 +20,18 @@ from pathlib import Path @dataclass class BlockEntry: """A tracked conversation block.""" - block_id: str # Short hex ID (first 8 chars of content hash) - content_hash: str # Full SHA-256 of content - size: int # Byte size of original content - turn: int # Turn when first seen - role: str # "user" or "assistant" - preview: str # First 80 chars for logging + + block_id: str # Short hex ID (first 8 chars of content hash) + content_hash: str # Full SHA-256 of content + size: int # Byte size of original content + turn: int # Turn when first seen + role: str # "user" or "assistant" + preview: str # First 80 chars for logging status: str = "resident" # resident | anchored | summarized | dropped original_content: str | None = None # Full content for fault restoration summary: str | None = None # Model-authored summary (if summarized) + collapse_start_turn: int | None = None + collapse_end_turn: int | None = None class BlockStore: @@ -108,8 +111,7 @@ class BlockStore: size_k = entry.size / 1024 block["text"] = f"[tensor:{entry.block_id} ({size_k:.1f}KB)]\n{text}" - def _get_or_create(self, content: str, role: str, - turn: int) -> BlockEntry | None: + def _get_or_create(self, content: str, role: str, turn: int) -> BlockEntry | None: """Get existing entry by content hash, or create a new one.""" content_hash = hashlib.sha256(content.encode()).hexdigest() short_id = content_hash[:8] @@ -181,8 +183,7 @@ class BlockStore: entry.status = "anchored" return True - def collapse_range(self, start_turn: int, end_turn: int, - summary: str) -> list[str]: + def collapse_range(self, start_turn: int, end_turn: int, summary: str) -> list[str]: """Replace all blocks in a turn range with a summary marker. Marks all resident/anchored blocks in [start_turn, end_turn] as @@ -194,8 +195,7 @@ class BlockStore: """ collapsed_ids = [] for entry in self._by_id.values(): - if (start_turn <= entry.turn <= end_turn - and entry.status in ("resident", "anchored")): + if start_turn <= entry.turn <= end_turn and entry.status in ("resident", "anchored"): entry.status = "dropped" collapsed_ids.append(entry.block_id) @@ -203,9 +203,7 @@ class BlockStore: return [] # Create a synthetic summary block for the range - synthetic_content = ( - f"[Turns {start_turn}-{end_turn} collapsed: {summary}]" - ) + synthetic_content = f"[Turns {start_turn}-{end_turn} collapsed: {summary}]" content_hash = hashlib.sha256(synthetic_content.encode()).hexdigest() short_id = content_hash[:8] @@ -223,6 +221,8 @@ class BlockStore: preview=synthetic_content[:80], status="summarized", summary=summary, + collapse_start_turn=start_turn, + collapse_end_turn=end_turn, ) self._by_id[short_id] = entry self._by_hash[content_hash] = short_id @@ -237,23 +237,67 @@ class BlockStore: Modifies messages in-place. Returns stats dict. """ stats = {"dropped": 0, "summarized": 0, "anchored": 0} + emitted_collapses: set[str] = set() + filtered_messages: list[dict] = [] for msg in messages: content = msg.get("content", "") if isinstance(content, str): - msg["content"] = self._apply_to_text(content, msg, stats) + new_text = self._apply_to_text(content, stats, emitted_collapses) + if not new_text.strip(): + continue + msg["content"] = new_text + filtered_messages.append(msg) elif isinstance(content, list): + new_blocks = [] for block in content: - if not isinstance(block, dict) or block.get("type") != "text": + if not isinstance(block, dict): + new_blocks.append(block) continue - text = block.get("text", "") - block["text"] = self._apply_to_text(text, msg, stats) + if block.get("type") != "text": + new_blocks.append(block) + continue + + text = block.get("text", "") + new_text = self._apply_to_text(text, stats, emitted_collapses) + if not new_text.strip(): + continue + block["text"] = new_text + new_blocks.append(block) + + if not new_blocks: + continue + msg["content"] = new_blocks + filtered_messages.append(msg) + + else: + filtered_messages.append(msg) + + messages[:] = filtered_messages return stats - def _apply_to_text(self, text: str, msg: dict, stats: dict) -> str: + def _find_collapse_summary(self, turn: int) -> BlockEntry | None: + """Return the newest synthetic collapse summary covering this turn.""" + for entry in reversed(list(self._by_id.values())): + if ( + entry.status == "summarized" + and entry.summary + and entry.collapse_start_turn is not None + and entry.collapse_end_turn is not None + and entry.collapse_start_turn <= turn <= entry.collapse_end_turn + ): + return entry + return None + + def _apply_to_text( + self, + text: str, + stats: dict, + emitted_collapses: set[str], + ) -> str: """Apply block status to a single text content.""" m = self._BLOCK_LABEL_RE.match(text) if not m: @@ -265,18 +309,28 @@ class BlockStore: return text if entry.status == "dropped": + collapse_summary = self._find_collapse_summary(entry.turn) + if collapse_summary is not None: + if collapse_summary.block_id not in emitted_collapses: + emitted_collapses.add(collapse_summary.block_id) + stats["summarized"] += 1 + start_turn = collapse_summary.collapse_start_turn + end_turn = collapse_summary.collapse_end_turn + return ( + f"[tensor:{collapse_summary.block_id} — summarized turns " + f"{start_turn}-{end_turn}]\n" + f"{collapse_summary.summary}" + ) + stats["dropped"] += 1 + return "" + stats["dropped"] += 1 turn_info = f"message {entry.turn} in session log" - return ( - f"[...archived {entry.size:,} chars, {turn_info}...]" - ) + return f"[...archived {entry.size:,} chars, {turn_info}...]" if entry.status == "summarized" and entry.summary: stats["summarized"] += 1 - return ( - f"[tensor:{block_id} — summarized, was {entry.size:,} chars]\n" - f"{entry.summary}" - ) + return f"[tensor:{block_id} — summarized, was {entry.size:,} chars]\n{entry.summary}" # resident or anchored — no change if entry.status == "anchored": @@ -290,14 +344,12 @@ class BlockStore: @property def total_bytes(self) -> int: - return sum(e.size for e in self._by_id.values() - if e.status == "resident") + return sum(e.size for e in self._by_id.values() if e.status == "resident") def large_blocks(self, min_size: int = 2000) -> list[BlockEntry]: """Return resident blocks larger than min_size, sorted by size.""" return sorted( - [e for e in self._by_id.values() - if e.status == "resident" and e.size >= min_size], + [e for e in self._by_id.values() if e.status == "resident" and e.size >= min_size], key=lambda e: e.size, reverse=True, ) @@ -325,16 +377,20 @@ class BlockStore: """ entries = [] for entry in self._by_id.values(): - entries.append({ - "block_id": entry.block_id, - "content_hash": entry.content_hash, - "size": entry.size, - "turn": entry.turn, - "role": entry.role, - "preview": entry.preview, - "status": entry.status, - "summary": entry.summary, - }) + entries.append( + { + "block_id": entry.block_id, + "content_hash": entry.content_hash, + "size": entry.size, + "turn": entry.turn, + "role": entry.role, + "preview": entry.preview, + "status": entry.status, + "summary": entry.summary, + "collapse_start_turn": entry.collapse_start_turn, + "collapse_end_turn": entry.collapse_end_turn, + } + ) tmp = path.with_suffix(".tmp") tmp.write_text(json.dumps(entries, indent=2)) @@ -369,6 +425,8 @@ class BlockStore: preview=rec["preview"], status=rec.get("status", "resident"), summary=rec.get("summary"), + collapse_start_turn=rec.get("collapse_start_turn"), + collapse_end_turn=rec.get("collapse_end_turn"), original_content=None, ) store._by_id[entry.block_id] = entry