feat: save conversation backups before compaction
Nicolas Leão committed
Mar 26, 2026 at 15:43 UTC
d42301a9b1fb8d7a28c21d13bff3d400c7b0efa5
1 file changed
+46
-11
plugins/_chat_compaction/helpers/compactor.py
+46
-11
@@ -1,16 +1,44 @@
1
"""Core compaction logic for the compaction plugin."""
2
import asyncio
3
+import os
4
+from datetime import datetime
5
from typing import Callable
6
7
from agent import Agent
6
-from helpers import history, tokens
8
+from helpers import files, history, tokens
9
from helpers.history import History, output_text
10
from helpers.log import Log
9
-from helpers.persist_chat import save_tmp_chat, remove_msg_files
11
+from helpers.persist_chat import (
12
+ export_json_chat,
13
+ get_chat_folder_path,
14
+ save_tmp_chat,
15
+ remove_msg_files,
16
+)
17
from helpers.state_monitor_integration import mark_dirty_all
18
from plugins._model_config.helpers.model_config import get_chat_model_config
19
20
21
+def _save_pre_compaction_backup(context, full_text: str) -> dict[str, str]:
22
+ """Save the original chat as JSON and plain text before compaction.
23
+
24
+ Returns dict with 'json' and 'txt' absolute file paths.
25
+ """
26
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
27
+ backup_dir = os.path.join(get_chat_folder_path(context.id), "backups")
28
+ os.makedirs(backup_dir, exist_ok=True)
29
+
30
+ json_path = os.path.join(backup_dir, f"pre-compact-{timestamp}.json")
31
+ txt_path = os.path.join(backup_dir, f"pre-compact-{timestamp}.txt")
32
+
33
+ json_content = export_json_chat(context)
34
+ with open(json_path, "w", encoding="utf-8") as f:
35
+ f.write(json_content)
36
+
37
+ with open(txt_path, "w", encoding="utf-8") as f:
38
+ f.write(full_text)
39
+
40
+ return {"json": json_path, "txt": txt_path}
41
+
42
43
async def run_compaction(context, use_chat_model: bool = True) -> None:
44
"""
@@ -74,31 +102,38 @@ async def run_compaction(context, use_chat_model: bool = True) -> None:
102
if not summary or not summary.strip():
103
raise ValueError("Compaction produced empty summary")
104
77
- # Step 5: Replace history with compacted version
78
- agent.history = History(agent=agent)
79
- agent.history.add_message(
80
- ai=True,
81
- content=f"## Context compacted\n\n{summary}"
105
+ # Step 5: Save pre-compaction backup before destroying history
106
+ backup_paths = _save_pre_compaction_backup(context, full_text)
107
+
108
+ # Step 6: Replace history with compacted version
109
+ backup_note = (
110
+ f"\n\n---\n"
111
+ f"*Pre-compaction backup of the full original conversation:*\n"
112
+ f"- `{backup_paths['txt']}`"
113
)
114
+ compacted_content = f"## Context compacted\n\n{summary}{backup_note}"
115
+
116
+ agent.history = History(agent=agent)
117
+ agent.history.add_message(ai=True, content=compacted_content)
118
119
# Clear subordinate chain
120
agent.data.pop(Agent.DATA_NAME_SUBORDINATE, None)
121
context.streaming_agent = None
122
88
- # Step 6: Reset log and create response
123
+ # Step 7: Reset log and create response
124
context.log.reset()
125
context.log.log(
126
type="response",
127
heading="Context compacted",
93
- content=f"## Context compacted\n\n{summary}",
128
+ content=compacted_content,
129
update_progress="none",
130
)
131
97
- # Step 7: Persist and notify
132
+ # Step 8: Persist and notify
133
save_tmp_chat(context)
134
remove_msg_files(context.id)
135
101
- # Step 8: Force progress bar to inactive state LAST
136
+ # Step 9: Force progress bar to inactive state LAST
137
# This must happen after all log operations and persist
138
context.log.set_progress("Waiting for input", 0, False)
139
mark_dirty_all(reason="plugins.compaction.compact_chat")