Skip to content

WithHandler type filter bypassed in generator-built handler chains #279

@proboscis

Description

@proboscis

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_withhandlerPASSES (direct nesting baseline)
  • test_type_filter_mediagen_stackFAILS (generator-built chain, actual mediagen stack)

Observed behavior

In mediagen's handler stack (built by _wrap_with_handler_bindings inside a @do generator):

  1. MemoFFmpegExtractAudioSegmentHandler (annotation: Effect, types=None) intercepts FFmpegExtractAudioSegment and yields CacheGetEffect
  2. CacheGetEffect is dispatched to replace_audio_handler (annotation: ReplaceAudioTrack, types=(ReplaceAudioTrack,))
  3. 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()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions