fix(memory): run end-of-turn sync off the turn thread (#41945)

A misconfigured/slow external memory provider could hold the agent in
the 'running' state for minutes after the final response was delivered.
MemoryManager.sync_all / queue_prefetch_all looped provider.sync_turn /
queue_prefetch INLINE on the turn-completion path; a provider making a
blocking network/daemon call (a broken Hindsight daemon was observed
blocking ~298s before failing) blocked run_conversation from returning.
Because every interface (CLI, TUI, gateway) marks the agent 'running'
until run_conversation returns, the agent stayed busy for the full block
and any follow-up message triggered an aggressive interrupt that dropped
the message.

Dispatch provider sync/prefetch to a lazily-created single-worker
background executor. sync_all / queue_prefetch_all return immediately;
work completes (or fails, logged) in the background. A single worker
serializes writes so turn N lands before turn N+1. flush_pending()
provides a barrier for session boundaries and deterministic tests.
shutdown_all() drains the executor with a bounded timeout so a wedged
provider can never hang teardown.

Builtin-only / no-provider sessions spawn no executor (zero new threads
in the common case).
This commit is contained in:
Teknium
2026-06-08 02:18:59 -07:00
committed by GitHub
parent a5c12f5f59
commit aa6f2775fa
4 changed files with 348 additions and 31 deletions
@@ -179,6 +179,7 @@ def test_sync_all_propagates_session_id_to_providers():
p = _RecordingProvider()
mm.add_provider(p)
mm.sync_all("hello", "world", session_id="sess-42")
mm.flush_pending(timeout=5)
assert p.sync_calls == [
{"user": "hello", "asst": "world", "session_id": "sess-42"}
]
@@ -189,6 +190,7 @@ def test_queue_prefetch_all_propagates_session_id_to_providers():
p = _RecordingProvider()
mm.add_provider(p)
mm.queue_prefetch_all("next query", session_id="sess-42")
mm.flush_pending(timeout=5)
assert p.queue_calls == [{"query": "next query", "session_id": "sess-42"}]