Skip to content

note_outcome() and note_adapter() silently fail when called before on_init() due to uninitialized _mode #1

Description

@Elshayib

Bug Report

Issue

In loopgain/funnel.py, the note_outcome() and note_adapter() methods check self._mode != _ENABLED but _mode is None until _ensure_loaded() is called. If these methods are called before on_init() or on_first_observe(), they incorrectly return early even when telemetry is enabled.

Location

loopgain/funnel.py lines ~630-660 (note_outcome and note_adapter methods)

Details

def note_outcome(self, state: str) -> None:
    """Record a terminal loop state into the coarse outcome distribution."""
    if self._mode != _ENABLED:  # BUG: _mode is None until _ensure_loaded() called
        return
    bucket = _STATE_TO_OUTCOME.get(state, "other")
    with self._lock:
        self._outcomes[bucket] = self._outcomes.get(bucket, 0) + 1

def note_adapter(self, name: Optional[str]) -> None:
    """Record which framework adapter is driving the loop."""
    if self._mode != _ENABLED or not name:  # BUG: same issue
        return
    self._adapter = str(name)

The Bug: self._mode is initialized as None in __init__ and only set by _ensure_loaded() (called from on_init() or on_first_observe()). If a framework adapter calls note_adapter() or note_outcome() before the first LoopGain construction triggers on_init(), the check fails and the telemetry data is lost.

Call Sequence That Triggers Bug

  1. Framework adapter initializes and calls note_adapter("langgraph")
  2. User creates LoopGain instance (triggers on_init())
  3. User runs loop, calls observe() (triggers on_first_observe())
  4. Loop ends, calls note_outcome("TARGET_MET")

Steps 1 and 4 fail silently because _mode is still None.

Suggested Fix

Call _ensure_loaded() at the start of both methods:

def note_outcome(self, state: str) -> None:
    if self._ensure_loaded() != _ENABLED:
        return
    bucket = _STATE_TO_OUTCOME.get(state, "other")
    with self._lock:
        self._outcomes[bucket] = self._outcomes.get(bucket, 0) + 1

def note_adapter(self, name: Optional[str]) -> None:
    if self._ensure_loaded() != _ENABLED or not name:
        return
    self._adapter = str(name)

Impact

  • Medium: Loss of adapter identification and outcome distribution data for affected runs
  • Affects any framework adapter that identifies itself before the first LoopGain construction
  • Silent failure — no error, just missing telemetry

Related

The on_init() and on_first_observe() methods correctly call _ensure_loaded(), but the other public hooks don't.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions