Repository navigation
Python: Keep sessions pending when a consolidation pass did not cover them - #9294
Conversation
… them Automatic consolidation cleared every pending session when a pass finished, including sessions whose memories were extracted after the pass took its topic snapshot. Those sessions stopped counting toward consolidation_min_sessions, so their memories waited for unrelated sessions before the next pass. A pass now retires only the sessions that were pending when it took its snapshot. after_run registers the session after extraction has written its topics, so a pass that sees a session as pending has also seen its memories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/review |
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (1 commit(s)): 4a53aa064987
Model: gpt-5.6-sol
Overview
The change closes two distinct-session races by registering sessions only after extraction and retiring only IDs covered by a consolidation snapshot. Owner-scoped state locks, per-topic locks, and transient-failure retention provide strong concurrency guardrails, and the new tests exercise the intended interleavings. However, delaying registration until after all extraction and index work creates a failure window where persisted memories can lose their consolidation count.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/core/agent_framework/_harness/_memory.py
…s cancelled Registering the session after extraction left it out of the pending list when a later topic write or the index rebuild failed, or the run was cancelled, after an earlier topic had already been written. Those topics then waited for unrelated sessions to trigger consolidation. Register the session in a `finally` once extraction is over, so it is still never visible as pending while its extraction is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pending-consolidation marker was the session id alone, de-duplicated. A second turn of a session that a running consolidation pass had already snapshotted as pending recorded nothing new, so when that pass succeeded it retired the session and dropped the later turn's topic out of the pending window. Agent.run does not serialize runs per AgentSession, so this can happen. Each finished turn now stores a fresh id for its session in `pending_turn_ids`. A pass snapshots those ids and retires a session only if its id is unchanged when the pass completes. `sessions_since_consolidation` keeps its format and still counts distinct sessions for `consolidation_min_sessions`; state without `pending_turn_ids` still loads, and a session pending without an id is retired by a pass that saw it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Shivam Sharma (@ShivamSharma43) One parser regression remains in python/packages/core/agent_framework/_harness/_memory.py: MemoryTopicRecord.from_markdown() now interprets Updated: and Sessions: as record metadata even inside ## Summary. A persisted summary line such as Sessions: keep the troubleshooting context. is removed from the summary and can overwrite session_ids. The base behavior limited those labels to the pre-section metadata area. Please restore the current_section is None guard and add round-trip coverage for metadata-looking summary lines.
…ion-pending-sessions
|
Eduard van Valkenburg (@eavanvalkenburg) thanks. That regression came from the branch predating #9079, not from a change here: this PR never touched |
MemoryContextProvider now normalizes the state returned by any MemoryStore, so a custom store that returns state saved before pending_turn_ids existed keeps working instead of raising KeyError. MemoryFileStore reuses the same normalizer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Shivam Sharma (@ShivamSharma43) The implementation concerns raised on this head look substantively addressed, but before approval we need to define the intended scope and design for cross-agent file-based memory—particularly ownership, isolation, and concurrency semantics when multiple agents share file-backed memory. I requested a plan on #9218 so we can understand and agree on that behavior first. Please hold this PR while that design is clarified. |
|
Thanks, holding this until the plan on #9218 settles. For reference, the PR doesn't change who owns the memory or how it is isolated. It only changes which pending session IDs a successful pass clears from the shared maintenance state: the IDs it saw when it took its topic snapshot. It also registers a session after its extraction rather than before. Once the design is agreed, I'll rework the PR to match. |
|
Shivam Sharma (@ShivamSharma43) The current-head review is clean under the accepted narrow scope. Before approval, please update the Description & Review Guide to reflect the implemented persisted |
|
Eduard van Valkenburg (@eavanvalkenburg) updated the Description & Review Guide for head c92fc7d. It now covers the persisted |
Motivation & Context
With automatic consolidation,
MemoryContextProviderkeeps the IDs of sessions that finished since the last pass insessions_since_consolidationand consolidates once there areconsolidation_min_sessionsof them. When a pass finished, it cleared that whole list, including sessions whose memories it never saw. Two orderings lose a session this way:after_runregistered the session before extracting its memories. If another session's pass took its topic snapshot while this session was still extracting, the pass covered the ID but not the memories, then cleared the ID. This is the schedule in Python: [Bug]: Automatic memory consolidation clears sessions still extracting memories #9218: B is registered, its extraction of aNewtopic is still running, and theOldpass clears B, so C leaves only["C"]pending and no consolidation is triggered.The facts themselves are saved either way. What gets lost is the count, so these memories wait for unrelated sessions before they are consolidated.
Description & Review Guide
_memory.py:pending_turn_ids, which maps each pending session ID to a freshuuid4marker for its latest finished turn.after_runrecords a new marker on every turn, even when the session is already pending._run_consolidationrecords the(session ID, turn marker)pairs it sees when it takes the topic snapshot. When the pass succeeds, it removes only the sessions whose marker is unchanged. A session that registered after the snapshot stays pending, and so does a session whose marker changed during the pass. The pass then keeps markers only for the sessions that remain. The empty-topics path clears both fields, as before.finally.after_runregisters the session (and its turn marker) after_extract_memoriesand the index rebuild instead of before. A pass that sees a session as pending has therefore also seen the topics that session wrote. Registration sits in afinally, so a run that fails or is cancelled after writing a topic is still counted. The exception or cancellation then propagates unchanged._normalize_statefills in missing or mistyped fields, including state saved beforepending_turn_idsexisted.MemoryFileStore.read_stateand the provider's own reads use it, so customMemoryStoreimplementations that return the old shape keep working. A session that is pending without a marker is retired by the next pass that sees it, which matches the previous behavior.pending_turn_idsfield in the state file. The transient-failure path still leaves the window and the queue unchanged. In the interleaved schedules above, the late session or late turn stays pending and counts toward the next pass. Two behavior changes:finallyregistration inafter_run._run_consolidation, which also handles overlapping turns of the same session.Agent.rundoesn't serialize runs per session, so a second turn can register while a pass that already counts the session is running. That second turn now stays pending; it is no longer an uncovered case.Tests in
tests/core/test_harness_memory.py:test_automatic_consolidation_keeps_session_extracting_during_pass: the schedule from the issue, with realAgent.runcalls on a gated client. B stays pending after theOldpass, and C then consolidatesNewtogether withOld.test_automatic_consolidation_keeps_session_extracting_before_snapshot: B starts extracting before another session's pass takes its snapshot. B stays pending, and the next pass consolidates B's fact.test_automatic_consolidation_keeps_overlapping_turn_of_covered_session_pending: a secondsession-1turn writes a topic and registers while a pass that snapshottedsession-1is running. The turn stays pending.test_memory_consolidation_retires_only_turns_in_its_snapshot: a turn registers while the pass is consolidating. It covers three cases: a new session, a session pending without a marker (legacy state) and a session pending with a marker. The pass advanceslast_consolidated_atand leaves only the late turn pending.test_memory_session_stays_pending_when_extraction_fails_after_a_topic_write: a run fails on the second topic write or on the index rebuild, after one topic was persisted. The error propagates and the session is pending.test_memory_session_stays_pending_when_extraction_is_cancelled_after_a_topic_write: the same check, for a run cancelled after one topic was written.test_memory_provider_accepts_custom_store_state_without_turn_idsandtest_memory_consolidation_accepts_custom_store_state_without_turn_ids: a custom store that returns state withoutpending_turn_idsstill registers sessions and retires them.With
_memory.pyfrom currentmainand this test file, the race tests fail: the during-pass, before-snapshot and overlapping-turn tests, all three cases of the retire test, and the cancellation test. The tests for failures after a topic write and for custom store state pass onmaintoo, becausemainregisters before extracting and has no markers. They guard against regressions in the new ordering.Checks run locally on Windows with Python 3.14, on head c92fc7d:
test_harness_memory.py,test_sessions.pyandtest_harness_background_agents.py: 277 passed, 1 skipped. I ran them with a short--basetemp, because onetest_sessions.pycase uses a filename longer than Windows' path limit under the default temp directory. That case is unrelated to this PR.ruff format/ruff check: clean._memory.py: no new diagnostics compared withmain, re-run on this head. Earlier heads were also checked with pyright, pyrefly, ty and zuban on the test file, with no new diagnostics.Related Issue
Fixes #9218
The issue is reproduced by the triage workflow and has no other open PR. It doesn't have a maintainer comment agreeing with the direction yet, so I've left that checklist item unchecked. I'm happy to adjust the approach if you'd prefer a different one.
AI Assistance
AI assistance details: analysis of the race, implementation, tests, and this PR description.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.🤖 Generated with Claude Code