Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

What it does

The hard-constraint filter (lib/arbitration/constraints.ts) drops offers that fully violate a budget, inventory, or frequency cap. Lagrangian arbitration runs after that filter and softly penalizes the survivors that are close to a cap, so traffic naturally rotates toward less-saturated offers. The penalty is computed from the dual variables (shadow prices λ_k) of a relaxed resource-allocation problem.

When it adds value

ScenarioHard filter aloneLagrangian on top
All caps with ample slackAll offers eligible, ranked by baseScoreSame — λ ≈ 0, scores unchanged
One offer at 95 % of daily capEligible until cap hits, then dropsSlight score penalty as it approaches cap → other offers rotate in
Cross-offer / channel quotasNot modeledModeled (W12, hot-path wire shipped) — see “Cross-offer constraints” below
W12 ships the cross-offer wire on the realtime /api/v1/recommend hot path: the rank node now combines per-offer Lagrangian constraints (budget + inventory) with cross-offer constraints (channel_quota, portfolio_budget, category_cap) in a single solve. The batch path (lib/batch-executor.ts) still uses the per-offer-only applyLagrangianAdjustment helper from W5.1; the hot-path realtime wire is the new applyRealtimeArbitration helper at lib/arbitration/realtime-wire.ts. Both share the same tenantSettings.aiAnalyzerSettings.arbitration.lagrangianEnabled flag and noOp/solverFailed semantics.

Configuration

The flag lives inside aiAnalyzerSettings:
{
  "aiAnalyzerSettings": {
    "arbitration": {
      "lagrangianEnabled": true
    }
  }
}
Default: off for every tenant. There is no global env-var kill switch yet — flipping the tenant flag back to false is the incident-response path.

What gets logged

Per batch run with the flag on, lib/batch-executor.ts emits a single aggregated log line at completion:
INFO lagrangian.applied {
  runId, tenantId,
  attempted, noOp, solverFailed, nonConverged,
  defaultedCostOffersCount, malformedConfigOffersCount,
  defaultedCostOffers: [<offerIds>],
  malformedConfigOffers: [<offerIds>]
}
FieldMeaning
attemptedCustomers where Lagrangian was invoked (post-contact-policy survivors > 0)
noOpCalls that returned without modeling any constraint (no offers had budget/inventory)
solverFailedCalls where the solver threw and we fell back to base scores
nonConvergedCalls where the solver ran but did not converge within tolerance
defaultedCostOffersOffers with budget configured but no per-impression cost — solver ran against a synthetic 1¢ unit cost
malformedConfigOffersOffers whose budget or inventory JSON had wrong types (string-encoded numbers, NaN, negatives)
If solverFailed > 0 or the tenant-settings lookup itself failed, the line is logged at error level instead of info. When the flag is off, nothing is logged for Lagrangian — no volume noise on tenants that haven’t opted in.

How the wire works

  1. The batch executor reads tenantSettings.aiAnalyzerSettings once per run to check whether Lagrangian arbitration is enabled.
  2. On every customer iteration, after the hard-constraint filter and before ranking, the Lagrangian adjustment runs against the surviving candidates.
  3. The helper builds one Lagrangian constraint per offer with non-zero remaining budget AND/OR non-zero remaining inventory.
  4. solveLagrangian runs dual ascent (200 iterations max, tolerance 1e-4, step size 0.1).
  5. Returned adjustedScores overwrite candidate.score for ranking.

Result shape

interface ApplyLagrangianResult {
  adjustedScores: Record<string, number>;
  shadowPrices: Record<string, number>;
  usage: Record<string, { used: number; rhs: number; slack: number }>;
  iterations: number;
  converged: boolean;
  /** Legitimate skip — no constraints to model. NOT set when the solver crashed. */
  noOp: boolean;
  /** Solver threw; baseScores were preserved. Distinct from noOp on purpose. */
  solverFailed: boolean;
  candidateCount: number;
  constraintCount: number;
  defaultedCostOfferIds: string[];
  malformedConfigOfferIds: string[];
}
noOp and solverFailed are deliberately separate. A “solver crashed” result is not a “no-op” — callers that conflate them will silently mask production failures.

Benchmark

platform/perf/lagrangian-vs-weighted.ts compares the Lagrangian path against a baseline weighted-composite ranker on 1000 synthetic customers × 10 offers (5 inventory-constrained, 5 unconstrained). Run:
cd platform
npx tsx perf/lagrangian-vs-weighted.ts
The first baseline shipped 2026-04-28 at platform/perf/baselines/2026-04-28-lagrangian-bench.json with the following honest result:
{
  "delta": {
    "revenueDeltaPct": 0,
    "giniDelta": 0,
    "latencyOverheadMs": 70.16
  }
}
Why zero deltas: the synthetic fixture only models per-offer constraints. Both paths reach the same selection because the hard filter already correctly handles “stock = 0” before the Lagrangian re-rank is consulted. The latency overhead (~70 µs per decision) is the cost of the dual-ascent loop on 5 binding constraints. This is a baseline, not a victory lap. The Lagrangian wire’s structural advantage will show when the offer model is extended to include channel-quota / portfolio / cross-offer constraints — those constraints couple multiple offers and the hard filter cannot correctly handle them in a single pass.

Operational checklist

  • Enable for one tenant first via the aiAnalyzerSettings.arbitration.lagrangianEnabled flag.
  • Watch the per-run lagrangian.applied log line. Specifically:
    • solverFailed > 0 → check the per-customer lagrangian adjustment threw errors and file an issue.
    • nonConverged > 0 → likely fine on small batches; investigate if it stays > 5 % of attempts.
    • malformedConfigOffersCount > 0 → fix the offer’s budget/inventory JSON shape (numbers, not strings).
    • defaultedCostOffersCount > 0 → solver ran but per-impression cost was unknown; results trustworthy only directionally.
  • If a regression appears, flip the flag back to false. The wire is designed so that flag-off behavior is bit-identical to pre-W5.1.

Cross-offer constraints (W12, realtime hot path)

A dedicated cross-offer-constraint table stores rules that span multiple offers. Three rule types ship today:
ruleTypeCap meaningCost vector
channel_quotaAt most N picks per channel in a window1 per pick of an offer whose channel is in config.channels
portfolio_budgetAt most $X spend across config.offerIdscostPerAction for offers in scope, 0 otherwise
category_capAt most M picks from offers in config.categories1 per pick in the category, 0 otherwise
Per-pick semantics. Cross-offer cost rows are assessed per candidate (per creative-pick), matching the cross-offer.ts test contract. Two creatives of the same offer each pay the cost — the intent is “n picks toward the cap when n creatives are picked.” A “count by offer” rule type is on the roadmap but not in the W12 scope.

Hot-path wire log

Per realtime recommend call with the flag on, lib/pipeline-runner.ts emits a single realtime arbitration applied log line at the rank node:
INFO realtime arbitration applied {
  tenantId, customerId,
  candidateCount,
  perOfferConstraintCount,
  crossOfferConstraintCount,
  noOp, solverFailed, converged, iterations,
  defaultedCostOfferIds, malformedConfigOfferIds
}
Field semantics match the batch-path lagrangian.applied line, with two additions: perOfferConstraintCount and crossOfferConstraintCount let you distinguish “no constraints to model” from “only cross-offer constraints active.”

Failure modes (fail-CLOSED)

ConditionBehavior
tenantSettings lookup failsWire stays OFF; error-level log surfaces the lookup failure.
Cross-offer rows lookup failsloadCrossOfferConstraints already returns [] on Prisma error and logs at warn; wire continues with per-offer constraints only.
Solver throwssolverFailed=true, adjustedScores === baseScores, error-level log. Ranking proceeds with original scores.
Wire helper itself throwsOuter try/catch in pipeline-runner falls through with original scores; error-level log surfaces the bug.
Flag offWire is fully skipped — no settings read on the hot path.
The realtime hot path NEVER returns a 5xx because of the wire — the worst case is degraded mode where scores are unchanged.

Roadmap

  • W5.2 ships the EXP3-IX online-weights / budget-pacing / goal-seek flags. They live in the same aiAnalyzerSettings.arbitration.* namespace.
  • A future iteration may add a “count by offer” cross-offer rule variant for tenants who want offer-pick semantics rather than creative-pick semantics. Today’s workaround is to limit candidates to one creative per offer upstream of the rank node.