A background memory/skill review (agent/background_review.py) forks a
second, complete AIAgent in a daemon thread that deliberately shares the
live agent's own session_id for prompt-cache warmth. Nothing previously
stopped a user's next live turn from starting while that fork was still
mid-conversation, letting both stream against the same session_id and
credentials concurrently. That produced two observable failures:
- Doubled prompt-token accounting on the live turn's own calls (the two
concurrent request/response streams under one session_id confuse the
token-usage bookkeeping), triggering premature context compression.
- A lockup that a normal interrupt could not clear: the review fork is a
fully independent AIAgent with its own _interrupt_requested flag, and
was never added to the parent's _active_children list -- the only list
AIAgent.interrupt() actually walks for cross-agent cancellation -- so a
live-turn Ctrl+C had no propagation path to it at all.
Fix, three files:
1. agent/agent_init.py -- add _background_review_agent /
_background_review_lock tracking state to every AIAgent, mirroring the
existing _active_children pattern.
2. agent/background_review.py -- the review fork now registers itself on
the parent's _active_children right after construction (reusing the
same list/lock interrupt() already fans out to for real subagent
delegation), and unregisters on every exit path (success, the
tool-whitelist finally, and the outer exception safety-net). All
registration is defensive (getattr/try-except) so an AIAgent built
without going through agent_init.py's setup degrades to "no
cross-turn cancellation" instead of aborting the whole review.
3. agent/conversation_loop.py -- at the very start of every
run_conversation() turn, if a prior background review is still
in-flight, it is now proactively cancelled via interrupt() before the
live turn proceeds -- fire-and-forget, non-blocking, adds no latency.
Adds 3 regression tests to tests/run_agent/test_background_review.py,
confirmed to fail against the pre-fix code via a scripted revert.
Verified: ruff clean on all touched files; 66/66 background-review and
interrupt-propagation tests pass; 256/256 across turn_finalizer +
run_agent regression suites; no fork-only symbols in the diff.