LemonRouter.RoutingFeedbackStore (lemon_router v0.1.0)

View Source

SQLite-backed store for routing feedback samples.

Records one row per finalized run, keyed by task fingerprint. Provides aggregate queries over outcome rates, durations, and sample sizes so the router can use past performance to break model-selection ties.

Design

  • Separate SQLite database (routing_feedback.sqlite3) — never touches the main store or run-history DB.
  • Writes are async casts (fire-and-forget). Read failures return {:insufficient_data, n} rather than raising.
  • A min_sample_size threshold (default Elixir.LemonRouter.RoutingFeedbackStore.min_sample_size/0) guards aggregates so the router never acts on statistically weak data.
  • The write path is gated before Bus publication by LemonMemory.Ingest; router reads are gated by the caller before consulting historical data.

Usage

# Called by the router-owned Bus subscriber when routing_feedback flag is on
RoutingFeedbackStore.record(fingerprint_key, outcome, duration_ms)

# Called by router to read aggregate stats
case RoutingFeedbackStore.aggregate(fingerprint_key) do
  {:ok, agg} -> agg.success_rate
  {:insufficient_data, _n} -> nil
end

Configuration

config :lemon_router, LemonRouter.RoutingFeedbackStore,
  path: "~/.lemon/store",          # directory — routing_feedback.sqlite3 created inside
  min_sample_size: 5,              # minimum samples before returning aggregate
  retention_ms: 30 * 24 * 3600_000 # 30 days (default)

Summary

Functions

Return aggregate stats for a fingerprint key.

Return the best-performing model for a context key (3-segment prefix).

Returns a specification to start this module under a supervisor.

List all fingerprint keys with summary stats.

Returns the configured minimum sample size threshold.

Record a routing feedback sample for a fingerprint key.

Return store-level summary statistics.

Functions

aggregate(fingerprint_key)

@spec aggregate(String.t()) :: {:ok, map()} | {:insufficient_data, non_neg_integer()}

Return aggregate stats for a fingerprint key.

Returns {:ok, agg} when enough samples are available, or {:insufficient_data, sample_count} when below the min_sample_size threshold.

Aggregate map

%{
  fingerprint_key: "code|bash,read_file|...",
  total: 12,
  outcomes: %{success: 8, partial: 2, failure: 2},
  success_rate: 0.667,
  mean_duration_ms: 4200
}

best_model_for_context(context_key)

@spec best_model_for_context(String.t()) ::
  {:ok, String.t()} | {:insufficient_data, 0}

Return the best-performing model for a context key (3-segment prefix).

Queries all fingerprint keys that share the same family|toolset|workspace prefix, aggregates success counts per model (across providers), and returns the model with the highest success rate that meets min_sample_size.

Returns {:ok, model_string} or {:insufficient_data, 0} if no model reaches the threshold.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

list_fingerprints()

@spec list_fingerprints() :: {:ok, [map()]} | {:error, term()}

List all fingerprint keys with summary stats.

Returns {:ok, [map()]} where each map has: fingerprint_key, total, success_count, avg_duration_ms, last_seen_ms.

Returns {:error, :not_running} if the GenServer is not available.

min_sample_size()

@spec min_sample_size() :: pos_integer()

Returns the configured minimum sample size threshold.

record(fingerprint_key, outcome, duration_ms \\ nil)

@spec record(String.t(), atom(), integer() | nil) :: :ok

Record a routing feedback sample for a fingerprint key.

Fire-and-forget cast. Returns :ok immediately. If the GenServer is not running, the call is silently swallowed (non-fatal).

start_link(opts \\ [])

store_stats()

@spec store_stats() :: {:ok, map()} | {:error, term()}

Return store-level summary statistics.

Returns {:ok, map()} with keys: total_records, unique_fingerprints, oldest_ms, newest_ms.