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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions