fix: render collapsed turn summaries in outbound context
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
235e88d416
commit
6719d3f3f0
1 changed files with 99 additions and 41 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue