Summary
The VM's annotation-driven type filter (SPEC-WITHHANDLER-TYPE-FILTER) does not reliably skip handlers when the WithHandler chain is built inside a @do generator. This causes cross-type effects to be dispatched to handlers whose type annotation should exclude them.
Reproduction
Branch: repro/withhandler-type-filter-bypass
Test: tests/test_withhandler_type_filter_in_generator.py
# Run from mediagen venv (requires mediagen installed):
uv run pytest tests/test_withhandler_type_filter_in_generator.py -v
test_type_filter_direct_chain_with_inner_withhandler — PASSES (direct nesting baseline)
test_type_filter_mediagen_stack — FAILS (generator-built chain, actual mediagen stack)
Observed behavior
In mediagen's handler stack (built by _wrap_with_handler_bindings inside a @do generator):
MemoFFmpegExtractAudioSegmentHandler (annotation: Effect, types=None) intercepts FFmpegExtractAudioSegment and yields CacheGetEffect
CacheGetEffect is dispatched to replace_audio_handler (annotation: ReplaceAudioTrack, types=(ReplaceAudioTrack,))
replace_audio_handler crashes with AttributeError: 'CacheGetEffect' object has no attribute 'video'
Expected behavior
CacheGetEffect should skip replace_audio_handler (since isinstance(CacheGetEffect, (ReplaceAudioTrack,)) is False) and reach cache_handler.
Investigation findings
- Type extraction is correct:
_extract_handler_effect_types(replace_audio_handler) returns (ReplaceAudioTrack,)
PyWithHandler IR node is correct: types=(ReplaceAudioTrack,) is stored and passed to the VM
classify_yielded_bound extracts types correctly: DoCtrl::WithHandler { types } is populated
should_invoke_handler logic is correct: checks isinstance(effect, type_tuple) and returns false for non-matching effects
- Direct nesting works: when the same handlers are wrapped with
WithHandler in plain Python (not inside a generator), the type filter works correctly
The bug only manifests when the WithHandler chain is constructed inside a @do generator and yielded as a sub-program — the pattern used by mediagen's _wrap_with_handler_bindings.
Mediagen topology
run(handlers=[default_handlers()])
wrap_with_mediagen_stack() ← @do generator
_wrap_with_handler_bindings() ← @do generator, resolves Ask, builds WithHandler chain
CacheHandler (types=(CacheGetEffect, CachePutEffect))
ShellHandler
...15 more domain handlers...
ReplaceAudioHandler (types=(ReplaceAudioTrack,)) ← BUG: receives CacheGetEffect
...
TranscribeHandler
MemoHandlers (×9) (types=None, yield CacheGetEffect)
WithHandler(transcribe_via_gemini_llm_handler, ...) ← program-level
program()
Summary
The VM's annotation-driven type filter (
SPEC-WITHHANDLER-TYPE-FILTER) does not reliably skip handlers when theWithHandlerchain is built inside a@dogenerator. This causes cross-type effects to be dispatched to handlers whose type annotation should exclude them.Reproduction
Branch:
repro/withhandler-type-filter-bypassTest:
tests/test_withhandler_type_filter_in_generator.py# Run from mediagen venv (requires mediagen installed): uv run pytest tests/test_withhandler_type_filter_in_generator.py -vtest_type_filter_direct_chain_with_inner_withhandler— PASSES (direct nesting baseline)test_type_filter_mediagen_stack— FAILS (generator-built chain, actual mediagen stack)Observed behavior
In mediagen's handler stack (built by
_wrap_with_handler_bindingsinside a@dogenerator):MemoFFmpegExtractAudioSegmentHandler(annotation:Effect,types=None) interceptsFFmpegExtractAudioSegmentand yieldsCacheGetEffectCacheGetEffectis dispatched toreplace_audio_handler(annotation:ReplaceAudioTrack,types=(ReplaceAudioTrack,))replace_audio_handlercrashes withAttributeError: 'CacheGetEffect' object has no attribute 'video'Expected behavior
CacheGetEffectshould skipreplace_audio_handler(sinceisinstance(CacheGetEffect, (ReplaceAudioTrack,))isFalse) and reachcache_handler.Investigation findings
_extract_handler_effect_types(replace_audio_handler)returns(ReplaceAudioTrack,)PyWithHandlerIR node is correct:types=(ReplaceAudioTrack,)is stored and passed to the VMclassify_yielded_boundextracts types correctly:DoCtrl::WithHandler { types }is populatedshould_invoke_handlerlogic is correct: checksisinstance(effect, type_tuple)and returns false for non-matching effectsWithHandlerin plain Python (not inside a generator), the type filter works correctlyThe bug only manifests when the
WithHandlerchain is constructed inside a@dogenerator and yielded as a sub-program — the pattern used by mediagen's_wrap_with_handler_bindings.Mediagen topology