> ## 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.

# Changelog

> Notable changes to the KaireonAI platform, in reverse chronological order. New capabilities land here when they are deployed to playground.kaireonai.com.

This is the platform changelog. It tracks notable user-facing changes to
KaireonAI — new features, APIs, UI additions, and behavioral shifts — in
reverse-chronological order. New capabilities land here when they are
deployed to `playground.kaireonai.com`.

For the forward-looking view, see the [roadmap](/roadmap).

***

## 2026-07-03 — Agentic AI: governed assistant, Recommendation Inbox, Autopilot, Sentinel, cold-start priors, hosted MCP

**Enforced preview→approve for every AI assistant write.** All entity-writing chat tools — including creates, model training, and predictor changes, which previously executed the moment the model called them — now return a preview card and execute only after explicit approval via the new `POST /api/v1/ai/mutations/confirm` (audit-logged, no LLM round-trip). See [AI Assistant](/ai-ml/ai-assistant#guided-autonomy).

**Recommendation Inbox on the AI sidebar.** The Segments, Policy Recommendations, and Content Intelligence pages now run their namesake LLM analyzers on demand and persist actionable findings as reviewable recommendations (deduplicated); AI Insights hosts the unified inbox. Applying creates draft entities — or performs scoped changes for the new `experiment` / `model` / `weights` types. A failed apply keeps the recommendation open. See [AI Insights](/ai-ml/ai-insights#recommendation-lifecycle).

**Decisioning Autopilot.** A 6-hour background sweep turns experiment champion/challenger results and significant model drift into inbox proposals, governed by a per-tenant autonomy mode: `suggest` (default), `auto_gated` (opens four-eyes Approval Requests), or `auto` (applies immediately, audit-logged). Configure in **Settings > AI Configuration > AI Autonomy**. See [Decisioning Autopilot](/ai-ml/autopilot).

**Decision Sentinel.** Two new alert metrics — `suppression_rate` and `empty_candidate_rate` — watch the decision funnel every 30 minutes; breaches raise System Health alerts, and hard breaches can auto-pause active flows for tenants that opt in. Both metrics are also available to custom alert rules and seeded as defaults for new tenants. See [Decision Sentinel](/ai-ml/sentinel).

**Cold-start priors.** New offers with a category seed informed propensity priors from same-category offers (10 pseudo-observations at the evidence-weighted neighbor mean) instead of a flat prior; real outcomes wash them out quickly and existing evidence is never overwritten. See [Adaptive Learning](/ai-ml/adaptive-learning#cold-start-prior-seeding).

**Hosted MCP endpoint.** `POST /api/v1/mcp` exposes the full MCP tool surface (172 tools + playbooks) over stateless JSON-RPC with API-key auth, per-call audit logging, tenant pinning, and governed playbook applies (mutations queue for approval instead of writing directly). See [MCP server](/ai-ml/mcp-flow-server#hosted-endpoint-no-local-process).

***

## 2026-07-03 — Decisioning correctness fixes + `offer_attribute` qualification rule; real logistic-regression training; hardened API validation

**New qualification rule type: `offer_attribute`.** Filters on **offer-level** fields (`productType`, `margin`, `revenueValue`, or custom fields) rather than customer attributes — the offer-side counterpart to `attribute_condition`. Same operator set, except `offer_attribute` supports `not_in` where `attribute_condition` supports `contains`. See [Qualification Rules](/decisioning/qualification-rules) and the [API reference](/api-reference/qualification-rules).

**`allow_override` contact policy fix.** An override configured with only `allowMandatory: true` previously un-blocked **every** candidate, not just mandatory Offers — a governance hole. `allowMandatory` alone now un-blocks mandatory Offers only; use `allowSegments`/`allowOfferIds` to gate by segment/offer, or an empty `{}` config for an intentional blanket override. See [Contact Policies](/decisioning/contact-policies#allow_override).

**`logistic_regression` now trains real weights.** The model fits genuine coefficients via full-batch gradient descent, consuming `learningRate` (0.05), `maxIterations` (200), and `regularization` (L2 λ, 0) from `model.config` — these were previously inert. A real fit needs **≥ 20 usable labeled rows spanning both classes**; below that, training falls back to a metrics-only pass rather than producing a degenerate model. See [Logistic Regression](/ai-ml/algorithms/logistic-regression#training).

**Hardened API validation.**

* **`POST /api/v1/outcome-types`** returns `409` (was `500`) when the `key` already exists for the tenant.
* **Offers and creatives now validate foreign-key tenant ownership.** A `categoryId`/`subCategoryId` (offers, sub-categories) or `offerId`/`channelId`/`placementId` (creatives) that doesn't belong to your tenant is rejected with `400` instead of being silently linked.
* **Mandatory-override guards now apply to updates.** Turning an Offer `mandatory` via `PUT /api/v1/offers` runs the same governance validation as create (future expiry, real in-tenant `admin` approver) — the update path can no longer bypass it.
* **`GET /api/v1/cron/scheduled-retrains`** returns `401` (was `503`) when `CRON_SECRET` is unset, consistent with the rest of the cron tier's fail-closed auth.

***

## 2026-06-08 — Cross-offer constraints CRUD API; realtime ranking flags configurable via analyzer-settings API and UI; analyzer-settings persistence fix

**Cross-offer constraints CRUD API is now public.** `GET/POST/PUT/DELETE /api/v1/cross-offer-constraints` lets operators create and manage cross-offer ranking constraints consumed by the realtime Lagrangian solver on `/recommend`. Three rule types are available: `channel_quota` (cap offers per channel), `portfolio_budget` (cap total spend across a set of offers), and `category_cap` (cap offers per category). Constraints are activated by enabling `tenantSettings.aiAnalyzerSettings.ranking.lagrangianEnabled`. See [Cross-Offer Constraints API](/api-reference/cross-offer-constraints).

**Realtime ranking flags are now configurable via `PUT /api/v1/ai/analyzer-settings`.** A new `ranking` object (validated by `RankingSettingsSchema`) exposes five boolean opt-in flags, all defaulting to `false`:

* **`lagrangianEnabled`** — gates the realtime Lagrangian shadow-price solver on the `/recommend` hot path. When on and any offer has a binding `budget.dailyCapCents`, the solver applies a continuous penalty that rotates traffic toward less-saturated offers without hard-dropping them. Cross-offer constraints are also loaded on this path when active rows exist. Verified against `src/lib/ranking/apply-lagrangian.ts` + `realtime-wire.ts`.
* **`crossOfferEnabled`** — gates cross-offer constraint loading on the **batch** path (`batch-executor.ts`). Requires `lagrangianEnabled: true`. Note: on the realtime `/recommend` path, cross-offer constraints are loaded whenever `lagrangianEnabled` is true — `crossOfferEnabled` is a batch-path-only gate. See [Cross-Offer Constraints API](/api-reference/cross-offer-constraints#runtime-paths) for the full runtime-path breakdown.
* **`exp3IxEnabled`** — gates the EXP3-IX online bandit. A no-op unless `banditArms` are operator-configured; see [EXP3-IX Ranking](/decisioning/ranking-exp3ix).
* **`budgetPacingEnabled`** and **`goalSeekEnabled`** — gating toggles for budget-pacing and goal-seek wires; full operator configuration UI is forthcoming. Enabling without operator config is a safe no-op.

The flags are read by `GET /api/v1/ai/analyzer-settings` (viewer+) and written by `PUT /api/v1/ai/analyzer-settings` (admin). The UI **Settings > AI Configuration** page surfaces them in the new **Ranking** card.

**Analyzer-settings persistence fix.** `PUT /api/v1/ai/analyzer-settings` now reads and writes the `TenantSettings` table — the same JSON column the `/recommend` runtime reads at decision time. Previously the route wrote to a table that the runtime did not consult, so saved settings had no effect on live decisions. Settings now take effect on the next `/recommend` call with no server restart required. Existing sibling keys in the `aiAnalyzerSettings` blob (e.g. `llmExplanationsEnabled`) are preserved across writes.

***

## 2026-06-07 — Security hardenings, key rotation, DSAR download, respond attribution fix, dashboard panels

**Five security hardenings shipped together.**

* **Timing-safe cron/trigger secret comparison** — every `CRON_SECRET` / shared-secret check across the cron and trigger routes now uses a constant-time comparison, eliminating a timing side-channel. The final three routes were migrated to close the gap: `cron/approvals-expire`, `cron/outbox-reaper`, and `triggers/file-arrival`.
* **Logger auto-redaction** — the platform logger automatically redacts sensitive keys (tokens, secrets, passwords, API keys, etc.) from structured log payloads so they cannot appear in log sinks.
* **MFA step-up proof is now server-issued.** A successful TOTP or WebAuthn verify mints an HMAC-SHA256-signed `kaireon_stepup` httpOnly cookie; middleware validates the cookie. The old `session.update({ mfaVerifiedAt })` path that let a client mint its own freshness is no longer trusted. See [MFA enforcement](/governance-security/mfa-enforcement).
* **API key scopes for SCIM.** `ApiKey.scopes` is now a JSON column (default `[]`). Non-empty scopes restrict a key to listed endpoints; SCIM endpoints require the `"scim"` scope. See [API Keys](/api-reference/api-keys) and [SCIM v2](/api-reference/scim).
* **Tenant-scoped profile lookup** — the customer profile route no longer returns cross-tenant data on an edge case.

**Encryption key rotation route.** New `POST /api/v1/encryption/rotate` (admin; MFA step-up for session admins, key-possession for API-key callers) re-encrypts four stores — connectors, platform settings, SSO OIDC secrets, MFA TOTP secrets — in a single call. Supports `dryRun: true` for pre-flight inspection. Row cap of 5,000 per store per pass; re-run for larger tenants. See [Encryption Key Rotation](/governance-security/key-rotation).

**DSAR deliverable export.** A new `dsar_exports` table (migration 28) persists export payloads at completion time. `GET /api/v1/dsar/{id}/download` (admin) returns the payload as a JSON file attachment. Encrypted exports are delivered as-is with `X-Kaireon-Payload-Encrypted: true`. Payloads age out on the "decisions" retention class; `DsarRequest` rows are never purged. Exports completed before 2026-06-07 are not downloadable — re-run the export. See [DSAR](/api-reference/dsar).

**Respond attribution fix.** `/respond` now honors `recommendationId` end-to-end. `/recommend` stamps the `recommendationId` column on recommendation and auto-impression rows. `/respond` uses a three-tier lookup ladder — (1) column match, (2) legacy JSON match for pre-fix rows, (3) rank-only fallback — so attribution remains precise for old rows. Limitation: multi-placement `/recommend` responses write no interaction rows, so precise attribution applies to single-flow recommendations and auto-impressions. See [Respond API](/api-reference/respond).

**Dashboard improvements.**

* **Operations — Decision Pipeline panel** now reads persisted `DecisionTrace` aggregates via `GET /api/v1/dashboards/decision-pipeline` (window 1–168 hours, default 24). The panel header shows the effective window from the API response. Filter rates are labeled as cumulative. The period selector correctly refetches the panel on change (was stuck at 7d). Empty state mentions `decisionTraceSampleRate`.
* **Model Health — four new Analysis panels:** Uplift (per-creative observational uplift with 95% CI, 7/30/90d window), CATE Explorer (per-customer τ with segment badges, model-picker + method select), Fairness (inline-mode paste-and-evaluate — no persisted history), Drift Check (paste reference/current distributions → PSI/KS per feature).
* **Attribution model picker** shows two disabled Coming Soon entries: Shapley and Cross-Device. The API enum is unchanged — five live models remain selectable.

***

## 2026-06-06 — Guardrail enforcement, four-eyes publish gate, CLV/uplift PRIE weights

**Guardrails are now enforced on every Recommend request.** Active
[guardrail rules](/api-reference/guardrails) run during the decision pipeline —
once per request at rank-node entry (so ranking and the result `limit` operate on
the survivors, and expressions can reference `offer.score`), with a response-node
fallback for flows without a rank node. A rule's `expressionAst` describes *when
it fires*: a firing `hard` rule **blocks** the candidate (audit-logged as
`mandatory_override`), a firing `soft` rule keeps it but logs a warning. A
malformed or unrecognized expression **never fires** — guardrails fail open at the
candidate level, so a mistyped rule degrades to a no-op rather than suppressing
every offer. Enforcement is independent of contact policies: `skipContactPolicy`
does not skip guardrails, and it is **not** gated by the `enableGuardrails`
module flag. Rules load fail-open (a load failure leaves the request unfiltered
with a loud log), are cached 300 s, and the cache is invalidated on every
guardrail create/update/delete. `debugTrace.afterGuardrails` is now the real
post-guardrail count, and `debugTrace.guardrailReasons[]` lists each failed
evaluation (`{ ruleKey, ruleName, severity, passed, reason?, offerId? }`).

**Four-eyes publish gate (opt-in).** The new tenant setting
[`requirePublishApproval`](/api-reference/tenant-settings) (default `false`,
toggle on the Settings page) gates
[decision-flow publish](/api-reference/decision-flows): when on, publish requires
a fresh approved [ApprovalRequest](/api-reference/approvals)
(`entityType=decisionFlow`, `action=publish`, matching `entityId`). One approval
authorizes exactly one publish — the publish stamps the approval id onto the new
version, so reuse is rejected. A blocked publish returns `422`
(`reason: "no_approval" | "consumed"`) and writes a `publish_blocked` audit entry.
The gate **fails closed** if the settings lookup errors. Because stage walking
already rejects self-approval and duplicate approvers, an approved request implies
two distinct identities. Migration:
`prisma/manual-sql/26_tenant_require_publish_approval.sql`.

**CLV and uplift weights in PRIE ranking (default 0 = no change).** The
[`formula` Score node](/decisioning/scoring-strategies) gains two optional
exponent terms outside the P+R+I+E sum-to-1 constraint: `upliftWeight` and
`clvWeight` (each `0..2` inline, `0..1` via a ranking profile). The
[ranking-profile](/api-reference/ranking-profiles) weight keys `uplift` and `clv`
now map straight into the formula — previously `upliftWeight` was documented but
stripped by validation, so it is now actually reachable. The CLV term applies
`score ×= impact^(clvWeight × clvNorm)`, where `clvNorm = clvScore/100` from the
customer's [CLV](/api-reference/clv) row, so high-CLV customers get up to
`clvWeight` extra impact emphasis (no CLV row → term skipped; lookup cached 300 s).
The linear multi-objective scorer (`computeArbitratedScore`) also gains a sixth
`clv` objective (default 0). Trace fields `clvNorm` and `clvImpactExponent` are
stamped per candidate when the term is active. Studio surfaces **Uplift** and
**CLV** sliders under Scoring Strategies.

***

## 2026-05-13 — Scoring fixes (#202 / #204 / #212), excludeJoinIds, implicit Contact Policy

**`model_adaptations` global-scope upsert (#202).** PostgreSQL treats
`NULL != NULL` in unique constraints, so the engine's
`ON CONFLICT (tenantId, modelId, scope, scopeId)` never fired on global
rows where `scopeId` was `NULL` — every Respond accumulated a duplicate
row instead of incrementing `positives/negatives/evidence`. Fixed by
using `""` as the global scope sentinel everywhere (respond, train,
reset). Read path tolerates both `""` and legacy `NULL` during rollout
via `a.scopeId ?? ""`. **Verified live:** 5 responds × 9 active models
now produce exactly 9 single global rows with `evidence=5` each (not 45
duplicate rows with `evidence=1`).

**Bandits + neural CF wiring to per-candidate scoring (#204).** In the
propensity scoring path, `scoreWithModel` expected
`attributes.offerIds` (plural) for Thompson and ε-greedy and
`attributes.{customerId, offerId}` for neural CF — but the engine
passed neither. All three model types short-circuited to a constant
`0.5` for every candidate regardless of model state. Fixed by
injecting `offerId`, `offerIds: [offerId]`, and `customerId` into the
per-candidate `scoringAttributes` before invoking `scoreWithModel`.
Bandits now sample per-arm; neural CF runs the embedding lookup; all
three respect their learned state.

**Maturity-ramp cold-start floor is tenant-configurable (#212).** The
implicit `0.20` floor inside `applyMaturityRamp` made per-customer A/B
testing unreliable: two customers comparing the same offer each rolled
their own deterministic-random seed, so there was a \~64% chance that
at least one customer saw the offer dropped. Default raised to `0.50`
(collision risk drops to 25%); new `maturityRampColdStartFloor` tenant
setting lets operators tighten back to `0.20` (legacy) or `0.0`
(strictest ramp), or disable the ramp entirely with
`modelMaturityThreshold = 0`.

**Per-flow auto-enrich opt-out (#168).** `EnrichNodeConfig.excludeJoinIds[]`
lets a single flow skip selected schema joins from `autoEnrich=true`
without disabling the join globally. The UI shows auto-enriched join
chips with an `×` button on the Enrich panel.

**Contact Policy is now implicit (#169).** Every flow runs the active
contact policy stage unless `DecisionFlow.skipContactPolicy = true`.
Operators no longer have to remember to drag a Policy node into every
flow — the engine injects one automatically. The opt-out exists for
test flows, transactional channels, and one-off promotional sends.

**Score / propensity hardening:**

* `propensityScoreFloor` (default `0.05`) prevents starvation —
  an offer with `positiveRate=0` can still earn a small exploration
  tail through `0.05 × fitMult`.
* `applyRankingInfluencers` now clamps boosted scores to `[0, 1]`
  so propensities above `1.0` never reach the Rank / Group / response
  payload.
* `no_action` outcomes correctly increment `evidence` (denominator)
  without touching `positives` or `negatives`.

**Off-by-one and reliability fixes:**

* `offer_category_cap` no longer over-counts — `InteractionSummary` now
  has an `offerCategory` denormalized column, and contact-policy
  evaluation requires an explicit match.
* `DecisionFlow.rankingProfileId` and `couplingOverride` plumb through
  the PUT API correctly.
* Draft / paused / archived flows are refused by the engine instead of
  being silently evaluated.

***

## 2026-05-12 (later) — Channel coupling, compute error surface, DNC

**Channel-level coupling replaces `Group.allowPartial`.** The legacy
all-or-nothing `allowPartial: false` switch was a coarse fail that
didn't distinguish between "empty due to operator's choice" and "empty
due to contact policy fatigue". It's now deprecated and a no-op (the
field still parses to keep existing IRs valid). Per-channel coupling
takes its place:

* `Channel.couplingMode` (`"partial"` default, `"atomic"` opt-in) — when
  atomic, an empty placement in this channel suppresses sibling
  placements **in the same channel only**. Cross-channel coupling is
  intentionally NOT supported — different channels are different
  attention surfaces.
* `DecisionFlow.couplingOverride` — per-flow override that beats the
  channel default. Useful when one channel serves both atomic flows
  (e.g. weekly digest email) and partial flows (e.g. transactional
  email).
* The post-group coupling pass writes `trace.summary.channelCoupling[]`
  so consumers can distinguish "we cascaded because X" from "this
  placement wasn't configured."

**Compute formula errors surface in the response.** Formula failures
(missing customer field, divide-by-zero, type mismatch) used to be
silently logged and the candidate's `personalization` key just went
missing. Now each failure lands in `personalization._errors[]` on the
candidate with `{ name, kind, formula, error }`, plus a count in
`trace.summary.computeErrors`. The candidate stays in the response —
operators can filter upstream if they want missing fields to drop the
candidate entirely.

**`do_not_contact` contact-policy ruleType is now properly wired.** It
was seeded by industry-accelerator templates but the contact-policy
engine had no explicit case — every DNC'd candidate fell through to the
fail-closed default branch with a misleading "Unknown rule type" error
log. Now there's an explicit `case "do_not_contact"` that blocks with a
clear reason and policy id. This is the only contact policy that
suppresses across channels (legal/regulatory boundary).

***

## 2026-05-12 — File-arrival triggers (push + poll) + blue\_green column cast

**Triggers are now first-class** — file\_arrival no longer waits inside a
scheduled run; the run itself is fired by the sentinel landing.

* **Poll path** — the in-process scheduler tick now sweeps every active
  pipeline with `trigger.kind === "file_arrival"` alongside its
  schedule sweep. It probes the source's configured path (via the same
  cloud-store wiring the source executor uses), matches keys against
  the trigger's per-pipeline `controlFilePattern`, debounces against
  `lastRunAt + debounceSeconds`, and dispatches the run in-process.
* **Push path** — new `POST /api/v1/triggers/file-arrival` endpoint
  accepts both native `{pipelineId, tenantId, objectKey}` payloads and
  S3 EventBridge envelopes. Self-authorizes via `CRON_SECRET`.
  Sub-second latency when S3 → EventBridge → API Destination is wired
  up.
* **Per-pipeline masks** — `controlFilePattern` lives on the trigger,
  so multiple pipelines watching the same inbox each declare their own
  sentinel (`customers.done`, `accounts.done`, `propositions.done`).
  Drop one → only the matching pipeline fires.
* **Sentinel cleanup** — after a successful fire, the sentinel is
  archived to `atomicity.successFolder` (or `failureFolder` on a
  failed run) so the same trigger doesn't re-fire on every tick.
* **Deadline enforcement** — `trigger.deadline.windowMinutes` /
  `onMiss` is now actually checked. `alert` emits a system-health
  warning; `fail` writes a synthetic failed `PipelineRun` so SLA
  dashboards count the miss; `skip` logs and continues. Dedup key is
  the pipeline's `lastRunAt` so the action fires once per missed
  window.

**Blue\_green load mode bug fix** — `runBlueGreen` previously did
`INSERT INTO <table>_new SELECT * FROM <staging>`, which Postgres
rejected with `42804` (`column "created_at" is of type timestamptz but
expression is of type text`) because staging columns are TEXT and
target columns are typed. The target executor now passes its
column-aware projection (`NULLIF + ::pgCastType`) into runBlueGreen, so
blue\_green produces the same explicit casts that append/truncate/upsert
do. A leftover `<table>_new` from a prior failed run is dropped before
the new CREATE-LIKE.

**LineageTab key warning** — the row map was using `<>...</>` shorthand
which doesn't accept props; switched to `<Fragment key={...}>` so React
stops warning about missing keys.

***

## 2026-05-02 (evening) — Schema-joins UX + customer profile lookup

* **Customer Lookup was returning "no customer found"** for valid
  IDs because the profile route only matched schemas with
  `entityType="customer"`. The schema-create form sets
  `schemaType="customer"` but leaves `entityType` at its default
  "custom", so the lookup missed the actual customer table. Fixed:
  the route now matches either field.
* **Schema-joins page redesigned around the customer-as-primary best
  practice.** Primary schema is no longer a picker — the tenant's
  customer schema is auto-resolved and shown as a read-only chip
  with its primary key. The foreign-schema dropdown excludes the
  customer schema (no self-joins). Foreign-key column auto-populates
  when a column matching the primary key exists, otherwise the user
  picks from a dropdown of the foreign schema's actual columns. The
  redundant primary-schema field picker was removed.

***

## 2026-05-02 (late PM) — Three bugs caught in testing

Surfaced during the IAM-role + custom-PK end-to-end test:

* **Schema detail no longer fakes an `id BIGSERIAL` row** when the
  user defined a custom PK. The list of AUTO columns now matches the
  actual `ds_*` table contents — `created_at` + `updated_at` only
  (when a custom PK is set), or those plus `id` (when no custom PK).
* **Pipeline Runs `rowsProcessed` was 4× the real count** because the
  run-handler reducer summed `rowsLoaded + rowsOut` across every
  node. Now it only sums target rows. Existing run records in the
  test tenant were backfilled.
* **Sample-row preview** evaluates the common single-call formulas
  inline (`concat`, `coalesce`, `min`, `max`, `round`, `abs`,
  identity reference). Complex formulas still defer to the runtime
  but with a friendlier placeholder. Default sample row no longer
  miscategorises `state` (and other fields containing "at") as a
  date.

Companion to the data-platform end-to-end walkthrough also added today:
[API Walkthrough](/api-reference/data-end-to-end-walkthrough).

***

## 2026-05-02 (also PM) — `{YYYY-MM-DD}` token expansion fix

Found during the same E2E test: archived files in S3 / GCS / Azure
were landing at literal `.archive/{YYYY-MM-DD}/` instead of
`.archive/2026-05-02/`. The local\_fs archive helper expands tokens,
but the cloud-store impls took the destination verbatim. The source
executor now expands `{YYYY-MM-DD}` / `{YYYYMMDD}` / `{YYYY}` /
`{MM}` / `{DD}` / `{HH}` / `{mm}` / `{ss}` before calling
`store.archive()`.

***

## 2026-05-02 (PM) — System Health + load-mode safety

Two coupled shipments in the same day:

**System Health widget** ([docs](/operations-reporting/system-health)). New top-bar
`Activity` icon — distinct from the bell — surfaces operational alerts
across the platform. DB-backed with per-user read state. New API at
`/api/v1/system-health` (GET cursor-paginated, PATCH read, POST
read-all, DELETE dismiss). Severity taxonomy `info | success | warning | error | critical`; 30-second polling that pauses on tab background;
`error`/`critical` route to existing external Slack/Teams/email
providers when configured. A retention purge cron at
`/api/v1/cron/system-health-purge` honors each tenant's retention
settings (data class `system_health`, default 90 days). The previously
dead bell icon + in-memory notification store are retired.

**Load-mode safety** ([docs](/data/pipelines/loading-modes-validation)). Real
data-loss footguns closed:

* `truncate` and `blue_green` now run an empty-source guard by
  default — pipelines no longer wipe live tables when the upstream
  produces 0 rows. Override per-target with `failOnEmptySource:
  false`.
* `truncate` and `incremental_watermark` wrap the destructive +
  INSERT statements in `prisma.$transaction` so a failed INSERT rolls
  back the TRUNCATE / watermark advance.
* `incremental_watermark` now persists the high-water in a
  `pipeline_watermarks` checkpoint table (falls back to
  `MAX(target.col)` on first run for backwards compat).
* `upsert` with all columns in `upsertKey` now throws at SQL build
  time instead of silently emitting `ON CONFLICT DO NOTHING`. UI +
  `parsePipelineIR` block the misconfiguration before save.
* TargetForm gains: full-refresh-shape hard warning recommending
  `blue_green` over `truncate`, `failOnEmptySource` toggle for
  destructive modes, mode-switch validation matrix, dedicated config
  panes for `upsertKey` / `watermarkColumn` / `cdcSource`.
* Source `onMissAction: alert` finally fires an actual alert into
  System Health.
* Optional `expectedRowCountDelta` per target node emits a `warning`
  alert when today's load is wildly outside the recent average.

DataSchema docs clarified: marking a schema field as the primary key
skips the auto-generated `id BIGSERIAL` column at table creation; the
runtime has supported user-defined PKs since the DDL helper landed.

Docs: [System Health](/operations-reporting/system-health) ·
[Loading Modes](/data/pipelines/loading-modes-validation) ·
[File Ingestion](/data/connectors/file-ingestion) · [Data Model](/data/schemas/data-model)

***

## 2026-05-02 — Flow editor UX cleanup

A focused pass cleaned up rough edges in the IR-native flow editor:

* **Lineage tab no longer 500s** on tables with `bigint` or Postgres
  `numeric`/`decimal` columns. The lineage payload now serialises
  large integers as strings (preserves precision past
  `Number.MAX_SAFE_INTEGER`) and renders Decimal columns in their
  human-readable form, so `lifetime_value` etc. render as `"10552.97"`
  instead of internal-state JSON. See
  [Flow Lineage](/data/observability/flow-lineage).
* **Pipeline Runs heading aligned with sidebar** — the standalone runs
  page H1 now matches the "Pipeline Runs" sidebar entry, the table
  uses fixed column widths via shadcn `<Table>`, and the Error column
  truncates with a `title=` tooltip for the full text.
* **Editor is now 2-pane** — the redundant "Recent runs" left pane was
  removed; the bottom strip + dedicated `/data/flow-runs` page cover
  run history. Center pane fills the freed width.
* **Branch node form** — `then` and `default` route inputs are
  dropdowns of existing IR nodes (excluding self + sources). Stale
  refs render with a red border.
* **Enrich node form** — output-field input now badges columns missing
  from the destination schema and offers a one-click "+ Add as
  \<dataType>" button that POSTs to `/api/v1/schemas/fields` with
  a sensible default type per provider.
* **Archive node form** — connector picker (Select limited to S3 / GCS
  / Azure / SFTP / `local_fs`), per-connector folder-creation help text
  (cloud=auto-create, SFTP/local=parent must exist), and a Test
  connection button reusing the existing `POST /api/v1/connectors/test`.
  IR `archiveNodeSchema` gains an optional `connectorId` field
  (backwards compatible — runtime executor still parses destination
  URLs until the cross-cutting wiring lands).
* **Transform + Validate sample-row preview** — collapsible widget
  inside both forms takes one JSON row and shows a per-op before/after
  diff (added/removed/changed fields highlighted) or per-rule pass/fail
  badge. Complex ops (`aggregate`, `lookup_join`, `vector_embed`,
  `geo_resolve`, `sentiment_score`, `language_detect`) and the
  `expression` op render as "preview-limited — run the pipeline" since
  they need server-side runtime context.

Docs: [Flow Editor UI](/data/pipelines/flow-editor-ui) ·
[Flow Lineage](/data/observability/flow-lineage)

***

## 2026-04-29 — Decisioning depth + ecosystem surfaces

A multi-pass sprint landed across 14 capability surfaces. Highlights:

* **Counterfactual training** — pre-train hook augments the
  `gradient_boosted` training set with synthetic neighbors of marginal
  rows. See [Counterfactual Training](/ai-ml/counterfactual-training).
* **Cross-offer ranking constraints** — a new constraint type with
  three rule shapes (`channel_quota`, `portfolio_budget`, `category_cap`)
  feeds the existing Lagrangian solver.
  See [Lagrangian ranking](/decisioning/ranking-lagrangian).
* **KernelSHAP** — `POST /api/v1/decisions/:id/shap` now dispatches
  `gradient_boosted` to TreeSHAP and `neural_cf` to KernelSHAP. See
  [SHAP](/ai-ml/shap).
* **Three new fairness metrics** — Gini coefficient, DeLong paired-AUC
  test, two-sample Kolmogorov-Smirnov. See
  [Advanced Fairness](/ai-ml/fairness-advanced).
* **Multi-stage four-eyes approvals + DSAR purge cron** — approvals
  now move through a sequence of named stages with per-stage state
  transitions; `GET /api/v1/cron/dsar-purge` sweeps decision traces,
  interaction history, and AI attachments past the strictest tenant
  retention setting. See
  [Governance four-eyes](/governance-security/governance-four-eyes).
* **Negotiation apply-mode + multi-turn sessions** — three new routes
  (`POST /api/v1/decisions/:id/negotiate/apply`, `POST /api/v1/negotiate/sessions`,
  `POST /api/v1/negotiate/sessions/:id/turn`) with a 7-gate apply
  pipeline. See [Negotiation Apply-Mode](/decisioning/negotiation-apply-mode).
* **26 new connector entries added** — registry expansion spanning
  CRM / MAP / CDP / audience-sync / helpdesk / workflow vendors. New
  entries ship as `coming_soon` form-only stubs.
* **4 new pipeline transforms** — `vector_embed`, `geo_resolve`,
  `sentiment_score`, `language_detect` with HTTP-pointed runtime
  adapters under `lib/flow/runtime/transforms/external-model-call.ts`.
* **SCIM 2.0 + WebAuthn + SIEM audit-log shipping** — `/scim/v2/Users`
  endpoints, full COSE-key parse with ES256 / RS256 assertion
  verification, and Splunk HEC / Datadog Logs / Elastic `_bulk`
  backends gated by SSRF validation.
* **Multi-region overlay** — `helm/values-multi-region.yaml` chart for
  2-region active-active topology, plus per-tenant region routing
  driven by a new tenant-region pinning table.
* **In-repo SDK + CLI + Postman + MCP scaffolds** — TypeScript SDK,
  Python SDK, `npx kaireon` CLI, Postman v2.1 collection, and MCP
  marketplace manifest under `sdks/`.
* **OpenAPI auto-discovery** — `tools/scripts/gen-openapi.mjs` walks
  every `app/api/v1/**/route.ts` and emits `platform/public/openapi.json`
  covering the full v1 surface.

Schema migration: `platform/prisma/manual-sql/09_parity_w11_to_w19.sql`
(idempotent) creates 5 new tables and backfills existing single-stage
approvals.

***

## 2026-04-17 — Action Insights + Reporting Platform

Four coordinated phases shipped as a single milestone: close analytics
gaps, make alerts actually fire, ship a full report builder and
scheduler, and deliver a C-suite executive dashboard with Export +
Save-as-Report across every view.

<Note>
  **Pilot deployment posture.** This release ships as **manual-only
  automation**. The alert evaluator, report scheduler, and scheduled
  report runner all run through `/api/cron/tick`, but `CRON_TOKEN` and AWS
  EventBridge are intentionally **not** wired during pilot to avoid
  runaway LLM / notification cost. Run Now buttons, Export buttons,
  ad-hoc notification sends, and on-demand alert evaluation all work
  unconditionally. See
  [EventBridge Setup](/self-host/deploy/eventbridge-setup) for the optional
  wiring path, and the [roadmap](/roadmap) for the pilot guardrails we
  plan to ship before enabling automation by default.
</Note>

### Phase 01 — Analytics Foundation

New analytical primitives that power every downstream surface in this
milestone.

* New `dashboard-data` case **`selection_frequency`** — per-offer
  `eligibleCount`, `scoredCount`, `selectedCount`, `selectionRate`,
  `avgRank`, and `rankDistribution[]`. Accepts `channelId`,
  `categoryId`, `decisionFlowId`, `segmentId` filters.
* New `dashboard-data` case **`anomaly_candidates`** — compares current
  vs. baseline period across acceptance rate, revenue, and degraded
  scoring rate; classifies severity (info / warning / critical) from
  z-score + absolute percent change.
* New `dashboard-data` case **`why_not_ranked`** — aggregate misses per
  offer: `scoredTooLow`, `filteredByContactPolicy`,
  `filteredByQualification`, and `beatenBy` (top-5 competing offers).
* **Segment dimension** added to `acceptance_rate`, `offer_performance`,
  `offer_performance_grouped`, `channel_effectiveness`, `daily_trend`,
  `revenue_trend`.
* Enriched decision-trace JSON shapes — structured `rejectionReason`,
  `rankBefore` / `rankAfter` on scoring results.
* Cross-decision narrative helpers that explain offer underperformance,
  segment coverage, and anomalies in plain language.

Docs: [AI Insights](/ai-ml/ai-insights) ·
[Dashboards](/operations-reporting/dashboards) ·
[Dashboard Data API](/api-reference/dashboard-data)

### Phase 02 — Notification Providers + AlertRule Execution

Pluggable notification system and live alert evaluation.

* **Pluggable notification provider interface** + registry with Slack,
  Microsoft Teams, outbound webhook, and Ops-Email (SES) adapters.
* New notifications tab in `/settings/integrations` — add / test /
  enable / disable / delete destinations.
* Encrypted credential storage in the platform-setting vault
  (AES-256-GCM); GET endpoints return redacted configs.
* An alert-rule evaluator compares observed vs. threshold over
  `windowMinutes`, derives severity, fans out to every destination in
  `channels`, and respects `cooldownMinutes`.
* New settings page **`/settings/alerts`** — CRUD for alert rules.
* New API surface:
  * `GET /api/v1/notifications/providers` and `POST /api/v1/notifications/providers`
  * `GET /api/v1/notifications/providers/:id`, `PATCH /api/v1/notifications/providers/:id`, `DELETE /api/v1/notifications/providers/:id`
  * `POST /api/v1/notifications/providers/:id/test`
  * `POST /api/v1/notifications/send`
  * `GET /api/v1/alerts` and `POST /api/v1/alerts` (alert rules — later renamed from `/alert-rules` to `/alerts`)
  * `GET /api/v1/alerts/:id`, `PUT /api/v1/alerts/:id`, `DELETE /api/v1/alerts/:id`
  * `POST /api/cron/tick` (token-authenticated; intended caller is an
    external scheduler such as AWS EventBridge).

Docs: [Notifications](/operations-reporting/notifications) ·
[Alert Rules](/operations-reporting/alert-rules) ·
[Notifications API](/api-reference/notifications) ·
[Alerts API](/api-reference/alerts) ·
[Cron API](/api-reference/cron) ·
[EventBridge Setup](/self-host/deploy/eventbridge-setup) *(optional during
pilot)*

### Phase 03 — Report Builder + Scheduler

User-configurable reports with LLM narration, four output formats, and
scheduled delivery through Phase 02 providers.

* New persistent objects for **report templates**, **report schedules**,
  and **report runs** (additive migration; no changes to existing data).
* Report data-source registry — 10 built-in sources (`offer_performance`,
  `channel_effectiveness`, `selection_frequency`, `anomaly_candidates`,
  `why_not_ranked`, `decision_traces_summary`, `funnel`, `revenue_trend`,
  `daily_trend`, `budget_burn`). Extension point: drop a new file in
  `src/lib/reports/data-sources/` and register it.
* Report format registry — built-in **PDF** (via `@react-pdf/renderer`),
  **CSV**, **Markdown**, **HTML**.
* **LLM narrative engine** — uses the tenant AI provider, produces an
  executive summary + per-section narratives + key takeaways, and caps
  input at \~5000 tokens worth of rows with explicit truncation signals.
* Report runner — loads the template, runs the configured data sources,
  calls the narrative engine, renders every requested format, records
  the run, and dispatches it to the configured destinations.
* Full API surface:
  * `GET|POST /api/v1/reports/templates`, `GET|PATCH|DELETE /api/v1/reports/templates/[id]`
  * `POST /api/v1/reports/templates/[id]/preview` (transient render, no persistence)
  * `POST /api/v1/reports/templates/[id]/run-now` (immediate run; works without the cron)
  * `GET|POST /api/v1/reports/schedules`, `PATCH|DELETE /api/v1/reports/schedules/[id]`
  * `GET /api/v1/reports/runs`, `GET /api/v1/reports/runs/[id]`
  * `GET /api/v1/reports/runs/[id]/artifacts/[format]`
* **`/settings/reports` builder UI** — compose sources, pick
  formats/narrative, schedule + destinations, live preview, runs
  history drawer.
* **`/api/cron/tick` extended** to also process due report schedules;
  response JSON gains `reportsEvaluated`, `reportsRan`, `reportErrors`.

Docs: [Reports](/operations-reporting/reports) ·
[Report Templates](/operations-reporting/report-templates) ·
[Report Schedules](/operations-reporting/report-schedules) ·
[First Scheduled Report guide](/tutorials/first-scheduled-report) ·
[Reports API](/api-reference/reports)

### Phase 04 — Executive Dashboard + Share-as-Report

C-suite-ready view and one-click sharing across every dashboard.

* New page **`/dashboards/executive`** — LLM-narrated weekly summary,
  six KPI cards with period-over-period deltas and sparklines, anomaly
  feed (last 7 days), segment × offer heatmap, and quick-links to
  operational dashboards.
* Reusable dashboard widgets:
  * **Period delta** — headline + Δ% vs. prior period + sparkline.
  * **Anomaly feed** — severity pill / metric / delta / explain button.
  * **Segment-by-offer heatmap** — top-10 × top-10 selection rate grid.
  * **Export menu** — PDF / CSV / Markdown / HTML dropdown (uses
    `/api/v1/reports/preview` under the hood; no cron required).
  * **Save-as-Report** — modal pre-filled from the current view; creates
    a report template plus a report schedule in one click.
* **Export + Save-as-Report wired into every dashboard** — Business,
  Operations, Model Health, Data Health, Attribution (in addition to
  Executive).
* Backend support for period-over-period (`summary_with_comparison`,
  `model_auc_summary_with_prev`, and sparklines on core metrics).

Docs: [Executive Dashboard](/operations-reporting/executive-dashboard) ·
[Dashboards](/operations-reporting/dashboards) ·
[Share a Dashboard guide](/tutorials/sharing-a-dashboard)

***

## Earlier changes

Earlier changes are tracked in commit history — see the
[platform repo](https://github.com/kaireonai). Notable recent work prior
to this milestone:

* **Mar–Apr 2026** — Docs remediation and sample-data corrections
  (connector count corrected to 24, transform count to 15, API response
  shapes aligned end-to-end with code).
* **Apr 2026** — Repo open-sourced under a single "Initial open source
  release" commit. CI and CodeQL workflows temporarily disabled pending
  the public repo cut-over.
* **Apr 2026** — API Explorer auto-creates an API key on first visit to
  the playground for streamlined onboarding.

This changelog starts with the 2026-04-17 milestone because that's when
we introduced it. For older history, consult the commit log.
