What it does
When a multi-turn negotiation session reachesstatus: "accepted", its
finalProposal represents the agreed terms (discount, term, bundle,
final price). Apply-mode promotes that proposal into the realtime
/api/v1/recommend response so downstream callers see the negotiated
terms alongside the next-best-action ranking.
Each ranked decision is decorated with one of:
appliedNegotiation: { sessionId, proposal }— every gate passed; the caller can present the negotiated terms.appliedNegotiationReject: { sessionId, reason }— a session existed but a gate blocked the apply (e.g., daily cap exceeded, regulator review still required, kill switch tripped).
Configuration
| Field | Default | Meaning |
|---|---|---|
applyModeEnabled | false | Master flag. When off, the wire short-circuits with zero I/O — no latency cost for non-opt-in tenants. |
regulatorReviewCleared | false | Only flip to true after the 30-day eval-harness run shows zero violations across the negotiation eval set. Without this flag every apply rejects with regulator_review_required. |
dailyApplyCap | 50 | Hard cap on auto-applies per tenant per day (UTC). |
killSwitchTenant | false | Per-tenant emergency stop. Trips immediately when set to true. |
killSwitchGlobal | false | Org-wide emergency stop, set centrally. |
recentValidationFailureRate | 0 | Rolling 0..1 ratio. When ≥ autoKillThreshold the gate auto-trips. |
autoKillThreshold | 0.2 | Threshold for the auto-trip above. |
Response shape
meta.negotiationApply is omitted when the wire is in noOp (flag off
or no relevant sessions found).
Reject reasons
reason | When |
|---|---|
apply_mode_disabled | The flag is off (rare — short-circuits earlier; surfaces only in audit rows). |
offer_not_negotiable | Offer.negotiable !== true. |
kill_switch_tripped | One of the three kill switches fired. The audit row’s changes.reject.source says which: tenant, global, or auto_error_rate. |
regulator_review_required | regulatorReviewCleared is false. |
apply_budget_exceeded | Today’s apply count ≥ dailyApplyCap. |
guardrail_violations | The session’s finalProposal re-validation failed. The audit row carries the violation list. |
guardrails_missing | Offer is negotiable=true but has no negotiationGuardrails configured — fail-CLOSED. |
Audit trail
Every apply and every reject writes one audit-log row with:action:"negotiate_apply_realtime"(apply) or"negotiate_apply_realtime_reject"(reject).entityType:"negotiation_session",entityId: the session id.changes:{ sessionId, applied, proposal | reject }.
appliesUsedToday from a count of these rows, so
the audit chain is the source of truth for daily-cap accounting.
Failure modes — fail-CLOSED
| Condition | Behavior |
|---|---|
tenantSettings lookup throws | Wire stays OFF; warn-level log surfaces the lookup failure; recommend response unchanged. |
| Offer / session lookup throws | Wire degrades to noOp; warn-level log surfaces it; recommend response unchanged. |
AuditLog.count throws | appliesUsedToday returns Number.MAX_SAFE_INTEGER so the daily-cap gate trips and every apply rejects with apply_budget_exceeded. Cap fails-CLOSED — an outage cannot silently lift the limit. Surfaces at error severity. |
Apply-row AuditLog.create throws | Logged at error (compliance teeth) — the apply decoration is still returned to the caller, but the absence of the audit row is escalated for reconciliation. |
Reject-row AuditLog.create throws | Logged at warn — informational. |
| Wire helper throws | The route’s try/catch returns the recommend response without decoration; the apply-mode failure never breaks ranking. |
Honest limits
-
Concurrency under-counting.
appliesUsedTodayis read-once + per-call increment in memory; the audit row is written fire-and-forget. Two simultaneous/recommendcalls for the same(tenant, day)can both read N and both apply up to cap, so the day total can exceed cap by(concurrency - 1) * applies_per_callbefore audit rows materialize. Acceptable for V1 because (a) the apply decoration is informational — no irreversible side effect — and (b) compliance retains the audit-log rows for retroactive reconciliation. A Redis atomic-counter increment with a TTL keyednegotiation:applies:{tenantId}:{YYYY-MM-DD}is on the roadmap and closes the gap. -
appliesUsedTodaycost.AuditLog.countis correct but slow under very high tenant volume. Same Redis follow-up addresses both this and #1. -
Placements path not wired. The multi-placement
/recommendresponse shape is decorated separately. Today the apply-mode wire only runs on the single-flow + auto-resolve paths. Wiring placements is a small follow-up.
Operational checklist
- Enable
applyModeEnabledfor one tenant first. Watch themeta.negotiationApplycounter on/recommendresponses and the AuditLog rows. - Run the negotiation eval harness for 30 nights. When
zeroViolationClearance >= 0.95for the full window, flipregulatorReviewClearedtotrue. - A regression in any of:
solverFailed > 0on therealtime ranking appliedlog line (see Lagrangian Ranking), sustainedkill_switch_tripped: { source: "auto_error_rate" }rejects, or AuditLog write failures aterrorseverity → flipapplyModeEnabledtofalseimmediately. The wire is designed so flag-off is bit-identical to the pre-W15 response.