Repository navigation
Python: fix(core): keep finished background tasks until cleared or released - #9387
Karthik Thota (karthik-0306) wants to merge 4 commits into
Conversation
…leased Part of microsoft#8760 ### Description Keep finished asyncio.Task objects in _RuntimeState.in_flight_tasks inside _finalize_task. They remain in memory until explicitly removed by background_agents_clear_completed_task or torn down via BackgroundAgentsProvider.release_session. ### Bug & Repro In the single-process implementation, _finalize_task extracted results and immediately removed the task from in_flight_tasks. However, host session state is persisted only after the agent run completes. If the host restores a session snapshot taken while a task was RUNNING (e.g. after a run failure, turn rollback, or host desync), subsequent calls to _refresh_task_state found no entry in in_flight_tasks and marked the task as LOST, silently discarding a completed or failed result. ### Trade-off Finished asyncio.Task objects remain referenced in in_flight_tasks until explicitly cleared via clear_completed_task or released via release_session, instead of being discarded immediately at finalization. ### Validation - pytest: 52 passed, 0 failed in 0.77s - ruff check: All checks passed - ruff format: 2 files formatted clean - pyright: 0 errors, 0 warnings, 0 informations - mypy: Success: no issues found
There was a problem hiding this comment.
🟡 Changes recommended
Reused task IDs can associate stale snapshots with results from a later continuation.
1 open finding
What changed in this PR
Retains finished background tasks to reconcile stale single-process session snapshots.
Changes:
- Stops removing tasks during finalization.
- Adds regression and lifecycle cleanup tests.
| File | Description |
|---|---|
_background_agents.py |
Retains finalized tasks and documents cleanup lifecycle. |
test_harness_background_agents.py |
Tests snapshot recovery, failures, cancellation, clearing, and release. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
…execution attribution (microsoft#8760) Address review finding on reusable task IDs in continuation flows: - Track a monotonic generation token on BackgroundTaskInfo and serialize it in to_dict/from_dict. - Dual-key _InFlightTasks with (task_id, generation) so each execution of a continued task is stored discretely. - _refresh_task_state reconciles RUNNING tasks against their specific generation, preventing a restored pre-continuation snapshot from erroneously receiving the result of a later continuation. - Clear and release paths clean up all executions associated with the task ID. - Add regression test test_reused_task_id_does_not_associate_stale_snapshot_with_continuation.
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Karthik Thota (@karthik-0306) background_agents_clear_completed_task can clear a newer running continuation when the caller restores an older completed generation of the same task id. The restored generation passes the non-running check, but runtime.clear_task(task_id) removes every (task_id, generation) entry and the shared background session. A newer generation can therefore keep running untracked; later refreshes mark it lost, and session release can no longer cancel or drain it.
Please make clear generation-aware so it removes only the completed generation, or explicitly reject/cancel newer running generations before removing their runtime handles. Add a regression that restores generation 1 while generation 2 is still running and then attempts to clear generation 1.
Addressed in 6b90a3d. Generation tracking is removed. background_agents_clear_completed_task now drops the runtime handle and background session only when the local task is done or absent. If a continuation is still running, its handle stays tracked, so release_session(cancel_running=True) can still cancel and drain it. The persisted record is still removed because the caller asked to clear it. Regression added: restore the completed snapshot while the continuation runs, clear, then confirm the continuation is still tracked and release cancels it. |
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Karthik Thota (@karthik-0306) The final head again keys retained runtime tasks only by task_id. background_agents_continue_task() overwrites that entry for a newer execution, while _refresh_task_state() reconciles any persisted RUNNING record against whichever task currently occupies the ID. A restored stale snapshot can therefore receive the result or error of a later continuation. Please preserve a serialized execution identity/generation (or otherwise prove the retained task is the same execution) and add a stale-snapshot-plus-continuation regression.
Persist a per-task generation so a restored older session snapshot is never reconciled against a later continuation that reused the same task ID. Continue/clear are generation-aware.
|
Eduard van Valkenburg (@eavanvalkenburg) Restored an execution identity. BackgroundTaskInfo now persists a generation (defaults to 1 for older state), and it goes up on every continuation. Runtime records which generation each tracked run belongs to, and refresh, wait and finalize only match a record's own generation. So a restored stale RUNNING snapshot becomes LOST instead of receiving a later continuation's result or error. Continuing from a stale snapshot is rejected while a newer run is still going, and otherwise moves past every generation seen. Clearing only releases runtime state when no newer generation exists. Added regression tests: restore a gen-1 RUNNING snapshot after gen 2 completes or fails, stale wait, and stale continue/clear. |

Motivation & Context
Why is this change required?
In the single-process
BackgroundAgentsProvider, session state is persisted by the host only after an agent run completes. If the host restores a session snapshot taken while a task wasRUNNING(e.g. after a run failure, turn rollback, or host state reload), any completed or failed task that had already been finalized in memory had itsasyncio.Taskpurged from_RuntimeState.in_flight_tasks. On subsequent turn invocations,_refresh_task_statecould not find the task inin_flight_tasksand misclassified it asLOST, discarding a result or error that had already completed successfully.What problem does it solve?
It ensures that completed/failed task instances remain available in memory so that a stale persisted
RUNNINGrecord can reconcile against the finished task and return the actual result or exception rather than reporting"Task state was lost".What scenario does it contribute to?
Single-process background agents executing across multi-turn sessions where host persistence timing, rollbacks, or intermediate turn failures could otherwise orphan completed tasks.
Issue Link:
Part of [Feature]: BackgroundAgentsProvider cannot tell a task that is running on another process from one whose owner died #8760 (Track 1 — single-process in-flight task retention).
Description & Review Guide
What are the major changes?
_background_agents.py: Removedruntime.in_flight_tasks.pop(task_info.id, None)from_finalize_task. Tasks are now retained inin_flight_tasksuntil explicitly removed viabackground_agents_clear_completed_taskor session teardown viaBackgroundAgentsProvider.release_session.test_harness_background_agents.py: Added 5 deterministic regression tests (usingawait taskinstead ofasyncio.sleep):"Task state was lost".clear_completed_taskexplicitly removes the task fromruntime.in_flight_tasks.release_sessionclears all in-flight tasks and releases the runtime state.What is the impact of these changes?
LOSTtask states caused by session persistence desync in single-process scenarios.asyncio.Taskreferences remain in memory for the duration of the session until cleared by the caller or released with the session.What do you want reviewers to focus on?
_finalize_taskvsclear_completed_task/release_session.test_harness_background_agents.py.Validation
52 passed, 0 failedin 0.77sRelated Issue
Part of #8760 (Does not close #8760; addresses Track 1 single-process gap while Track 2 cross-process ADR remains open).
AI Assistance
AI assistance details:
AI assisted with drafting regression tests, formatting docstrings, running validation checks (pytest, ruff, pyright, mypy), and drafting this PR description.
Contribution Checklist