offload memory to deferred tasks

frdel committed Feb 1, 2026 at 20:26 UTC 720ff4a5eae911b816b7826a498a8048c5bace73
6 files changed +27 -16
python/extensions/message_loop_end/_10_organize_history.py
+5 -4
@@ -1,6 +1,6 @@
1 -import asyncio
1 from python.helpers.extension import Extension
2 from agent import LoopData
3 +from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
4
5 DATA_NAME_TASK = "_organize_history_task"
6
@@ -8,11 +8,12 @@ DATA_NAME_TASK = "_organize_history_task"
8 class OrganizeHistory(Extension):
9 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
10 # is there a running task? if yes, skip this round, the wait extension will double check the context size
11 - task = self.agent.get_data(DATA_NAME_TASK)
12 - if task and not task.done():
11 + task: DeferredTask|None = self.agent.get_data(DATA_NAME_TASK)
12 + if task and not task.is_ready():
13 return
14
15 # start task
16 - task = asyncio.create_task(self.agent.history.compress())
16 + task = DeferredTask(thread_name=THREAD_BACKGROUND)
17 + task.start_task(self.agent.history.compress)
18 # set to agent to be able to wait for it
19 self.agent.set_data(DATA_NAME_TASK, task)
python/extensions/message_loop_prompts_before/_90_organize_history_wait.py
+5 -5
@@ -1,7 +1,7 @@
1 from python.helpers.extension import Extension
2 from agent import LoopData
3 from python.extensions.message_loop_end._10_organize_history import DATA_NAME_TASK
4 -import asyncio
4 +from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
5
6
7 class OrganizeHistoryWait(Extension):
@@ -10,20 +10,20 @@ class OrganizeHistoryWait(Extension):
10 # sync action only required if the history is too large, otherwise leave it in background
11 while self.agent.history.is_over_limit():
12 # get task
13 - task = self.agent.get_data(DATA_NAME_TASK)
13 + task: DeferredTask|None = self.agent.get_data(DATA_NAME_TASK)
14
15 # Check if the task is already done
16 if task:
17 - if not task.done():
17 + if not task.is_ready():
18 self.agent.context.log.set_progress("Compressing history...")
19
20 # Wait for the task to complete
21 - await task
21 + await task.result()
22
23 # Clear the coroutine data after it's done
24 self.agent.set_data(DATA_NAME_TASK, None)
25 else:
26 - # no task running, start and wait
26 + # no task was running, start and wait
27 self.agent.context.log.set_progress("Compressing history...")
28 await self.agent.history.compress()
29
python/extensions/monologue_end/_50_memorize_fragments.py
+7 -1
@@ -6,6 +6,7 @@ from python.helpers.dirty_json import DirtyJson
6 from agent import LoopData
7 from python.helpers.log import LogItem
8 from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9 +from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
10
11
12 class MemorizeMemories(Extension):
@@ -25,11 +26,16 @@ class MemorizeMemories(Extension):
26 )
27
28 # memorize in background
28 - task = asyncio.create_task(self.memorize(loop_data, log_item))
29 + task = DeferredTask(thread_name=THREAD_BACKGROUND)
30 + task.start_task(self.memorize, loop_data, log_item)
31 + # task = asyncio.create_task(self.memorize(loop_data, log_item))
32 return task
33
34 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
35
36 +
37 + await asyncio.sleep(15)
38 +
39 set = settings.get_settings()
40
41 db = await Memory.get(self.agent)
python/extensions/monologue_end/_51_memorize_solutions.py
+4 -2
@@ -6,7 +6,7 @@ from python.helpers.dirty_json import DirtyJson
6 from agent import LoopData
7 from python.helpers.log import LogItem
8 from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9 -
9 +from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
10
11 class MemorizeSolutions(Extension):
12
@@ -25,7 +25,9 @@ class MemorizeSolutions(Extension):
25 )
26
27 # memorize in background
28 - task = asyncio.create_task(self.memorize(loop_data, log_item))
28 + task = DeferredTask(thread_name=THREAD_BACKGROUND)
29 + task.start_task(self.memorize, loop_data, log_item)
30 + # task = asyncio.create_task(self.memorize(loop_data, log_item))
31 return task
32
33 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
python/helpers/defer.py
+5 -3
@@ -6,17 +6,19 @@ from typing import Any, Callable, Optional, Coroutine, TypeVar, Awaitable
6
7 T = TypeVar("T")
8
9 +THREAD_BACKGROUND = "Background"
10 +
11
12 class EventLoopThread:
13 _instances: dict[str, "EventLoopThread"] = {}
14 _lock = threading.Lock()
15
14 - def __init__(self, thread_name: str = "Background") -> None:
16 + def __init__(self, thread_name: str = THREAD_BACKGROUND) -> None:
17 """Initialize the event loop thread."""
18 self.thread_name = thread_name
19 self._start()
20
19 - def __new__(cls, thread_name: str = "Background"):
21 + def __new__(cls, thread_name: str = THREAD_BACKGROUND):
22 with cls._lock:
23 if thread_name not in cls._instances:
24 instance = super(EventLoopThread, cls).__new__(cls)
@@ -81,7 +83,7 @@ class ChildTask:
83 class DeferredTask:
84 def __init__(
85 self,
84 - thread_name: str = "Background",
86 + thread_name: str = THREAD_BACKGROUND,
87 ):
88 self.event_loop_thread = EventLoopThread(thread_name)
89 self._future: Optional[Future] = None
webui/js/messages.js
+1 -1
@@ -1217,7 +1217,7 @@ export function drawMessageUtil({
1217 });
1218
1219
1220 - result.dontScroll = preferencesStore.showUtils;
1220 + result.dontScroll = !preferencesStore.showUtils;
1221 return result;
1222 }
1223