diff --git a/backend/packages/harness/deerflow/runtime/AGENTS.md b/backend/packages/harness/deerflow/runtime/AGENTS.md index baccfa8ea6..dc491e0806 100644 --- a/backend/packages/harness/deerflow/runtime/AGENTS.md +++ b/backend/packages/harness/deerflow/runtime/AGENTS.md @@ -100,7 +100,10 @@ producers (currently slash-skill activation via `asyncio.to_thread`) schedule journal mutation directly onto its owning event loop; they never mutate or flush `RunJournal._buffer` from the worker thread. The task-tool subagent proxy rejects a loop that differs from the journal owner, so its close fence always -drains the only scheduling hop. +drains the only scheduling hop. `flush()` yields to the loop once before +draining: since Python 3.13 an awaited executor future can complete without a +loop iteration, so a hop queued by a worker that already returned may still be +pending when flush starts. The persisted projection accepts only framework-defined error/action values and strict booleans (using null for invalid values) from the producer-supplied tool stamp; tool content, args, diff --git a/backend/packages/harness/deerflow/runtime/journal.py b/backend/packages/harness/deerflow/runtime/journal.py index ae5076d7ea..9fbae74a53 100644 --- a/backend/packages/harness/deerflow/runtime/journal.py +++ b/backend/packages/harness/deerflow/runtime/journal.py @@ -1352,6 +1352,15 @@ class RunJournal(BaseCallbackHandler): async def flush(self) -> None: """Force flush remaining buffer. Called in worker's finally block.""" + if self._closed: + return + # Events recorded from worker threads reach this loop through + # call_soon_threadsafe (see record_middleware). Since Python 3.13 an + # awaited run_in_executor()/to_thread() may complete without yielding + # to the loop when the worker finished before its future was chained, + # so such callbacks can still be queued when flush() starts. Yield once + # so they land in the buffer instead of being dropped after detach. + await asyncio.sleep(0) if self._closed: return self._explicit_flush_in_progress = True