Repository navigation
Conversation
|
Sibling PR #108240 (Xipong) addresses the same issue — posting an honest comparison for the maintainer, timeline first. Timeline
Both PRs implement the same core contract from the issue:
The maintainer-relevant trade-off, stated plainly: #108240 avoids the serialization peak and keeps unusual payload types alive at the cost of a graph-walking accountant; this PR pays a transient Happy to defer to #108240 if the graph-accounting direction is preferred, or to rebase my regression tests onto it — the budget/eviction/weakref test surface is nearly identical and the two changes don't compose (same |
… frozen snapshots Queued background reviews now hold the parent agent weakly and the transcript clone as frozen UTF-8 JSON bytes, under an aggregate byte/session budget that evicts the oldest best-effort entries under pressure. Oversized or non-serializable snapshots are rejected at enqueue; unreadable payloads drop at dispatch. Fixes NousResearch#108234.
What
agent/review_idle_queue.ReviewIdleQueuekept every deferred review alive with two unbounded strong owners: the parentAIAgentinstance and a structural clone of the whole transcript (messages_snapshot). On a busy managed-local host one such entry could sit per session for the full defer window (default 30 min), pinning agents that the cache had already evicted plus every large immutable tool-result string in the clone (#108234).Change — all at the queue boundary, no call-site changes
_PendingReviewnow holds the parent throughweakref.ref(objects that cannot be weak-referenced degrade to a dead ref — the review drops at dispatch). A collected parent drops the best-effort review with a log line instead of extending the lifetime of clients, tool state and provider state.enqueue(json.dumps(..., ensure_ascii=False)), detaching the queue from the transcript's nested object graph. It is thawed back into a message list only after an item is popped, so dispatch semantics (_spawn_background_review_now(**kwargs)) are unchanged; the JSON round-trip preserves exact message data._snapshot_bytes) is maintained under the existing lock: coalescing replaces the previous entry's bytes, popping releases them immediately._SNAPSHOT_BUDGET_BYTES/_SESSION_BUDGET): pressure evicts the oldest entries with a warning; a single snapshot larger than the whole budget is rejected rather than retained forever; non-serializable snapshots are rejected at enqueue. Background review stays best-effort and never outranks foreground memory ownership._dispatch(), which keeps the existing enabled-gate recheck and adds fail-closed handling: dead parent or unreadable frozen payload → drop + warn, never poison the thread.Tests
tests/agent/test_review_idle_queue.pygrew from 19 to 28 tests, covering every regression item from the issue: frozen bytes (not the transcript list) in pending entries, weakref clearing after the last external reference dies, coalesce-replaces-accounting, byte- and session-budget eviction of the oldest entry, oversized/non-serializable rejection, dispatch round-trip reconstructing the message list with accounting back to zero, and corrupt-payload fail-closed. All pre-existing 19 tests still pass unchanged (one assertion updated to the new frozen-snapshot contract).Fixes #108234