playground.kaireonai.com.
For the forward-looking view, see the 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 newPOST /api/v1/ai/mutations/confirm (audit-logged, no LLM round-trip). See AI Assistant.
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.
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.
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.
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.
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.
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 and the API reference.
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.
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.
Hardened API validation.
POST /api/v1/outcome-typesreturns409(was500) when thekeyalready exists for the tenant.- Offers and creatives now validate foreign-key tenant ownership. A
categoryId/subCategoryId(offers, sub-categories) orofferId/channelId/placementId(creatives) that doesn’t belong to your tenant is rejected with400instead of being silently linked. - Mandatory-override guards now apply to updates. Turning an Offer
mandatoryviaPUT /api/v1/offersruns the same governance validation as create (future expiry, real in-tenantadminapprover) — the update path can no longer bypass it. GET /api/v1/cron/scheduled-retrainsreturns401(was503) whenCRON_SECRETis 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.
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/recommendhot path. When on and any offer has a bindingbudget.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 againstsrc/lib/ranking/apply-lagrangian.ts+realtime-wire.ts.crossOfferEnabled— gates cross-offer constraint loading on the batch path (batch-executor.ts). RequireslagrangianEnabled: true. Note: on the realtime/recommendpath, cross-offer constraints are loaded wheneverlagrangianEnabledis true —crossOfferEnabledis a batch-path-only gate. See Cross-Offer Constraints API for the full runtime-path breakdown.exp3IxEnabled— gates the EXP3-IX online bandit. A no-op unlessbanditArmsare operator-configured; see EXP3-IX Ranking.budgetPacingEnabledandgoalSeekEnabled— gating toggles for budget-pacing and goal-seek wires; full operator configuration UI is forthcoming. Enabling without operator config is a safe no-op.
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, andtriggers/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_stepuphttpOnly cookie; middleware validates the cookie. The oldsession.update({ mfaVerifiedAt })path that let a client mint its own freshness is no longer trusted. See MFA enforcement. - API key scopes for SCIM.
ApiKey.scopesis now a JSON column (default[]). Non-empty scopes restrict a key to listed endpoints; SCIM endpoints require the"scim"scope. See API Keys and SCIM v2. - Tenant-scoped profile lookup — the customer profile route no longer returns cross-tenant data on an edge case.
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.
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.
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.
Dashboard improvements.
- Operations — Decision Pipeline panel now reads persisted
DecisionTraceaggregates viaGET /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 mentionsdecisionTraceSampleRate. - 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 run during the decision pipeline — once per request at rank-node entry (so ranking and the resultlimit 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 (default false,
toggle on the Settings page) gates
decision-flow publish: when on, publish requires
a fresh approved ApprovalRequest
(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 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 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 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(default0.05) prevents starvation — an offer withpositiveRate=0can still earn a small exploration tail through0.05 × fitMult.applyRankingInfluencersnow clamps boosted scores to[0, 1]so propensities above1.0never reach the Rank / Group / response payload.no_actionoutcomes correctly incrementevidence(denominator) without touchingpositivesornegatives.
offer_category_capno longer over-counts —InteractionSummarynow has anofferCategorydenormalized column, and contact-policy evaluation requires an explicit match.DecisionFlow.rankingProfileIdandcouplingOverrideplumb 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 replacesGroup.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.”
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-pipelinecontrolFilePattern, debounces againstlastRunAt + debounceSeconds, and dispatches the run in-process. - Push path — new
POST /api/v1/triggers/file-arrivalendpoint accepts both native{pipelineId, tenantId, objectKey}payloads and S3 EventBridge envelopes. Self-authorizes viaCRON_SECRET. Sub-second latency when S3 → EventBridge → API Destination is wired up. - Per-pipeline masks —
controlFilePatternlives 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(orfailureFolderon a failed run) so the same trigger doesn’t re-fire on every tick. - Deadline enforcement —
trigger.deadline.windowMinutes/onMissis now actually checked.alertemits a system-health warning;failwrites a synthetic failedPipelineRunso SLA dashboards count the miss;skiplogs and continues. Dedup key is the pipeline’slastRunAtso the action fires once per missed window.
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 setsschemaType="customer"but leavesentityTypeat 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 BIGSERIALrow when the user defined a custom PK. The list of AUTO columns now matches the actualds_*table contents —created_at+updated_atonly (when a custom PK is set), or those plusid(when no custom PK). - Pipeline Runs
rowsProcessedwas 4× the real count because the run-handler reducer summedrowsLoaded + rowsOutacross 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 miscategorisesstate(and other fields containing “at”) as a date.
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). New top-barActivity 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). Real
data-loss footguns closed:
truncateandblue_greennow run an empty-source guard by default — pipelines no longer wipe live tables when the upstream produces 0 rows. Override per-target withfailOnEmptySource: false.truncateandincremental_watermarkwrap the destructive + INSERT statements inprisma.$transactionso a failed INSERT rolls back the TRUNCATE / watermark advance.incremental_watermarknow persists the high-water in apipeline_watermarkscheckpoint table (falls back toMAX(target.col)on first run for backwards compat).upsertwith all columns inupsertKeynow throws at SQL build time instead of silently emittingON CONFLICT DO NOTHING. UI +parsePipelineIRblock the misconfiguration before save.- TargetForm gains: full-refresh-shape hard warning recommending
blue_greenovertruncate,failOnEmptySourcetoggle for destructive modes, mode-switch validation matrix, dedicated config panes forupsertKey/watermarkColumn/cdcSource. - Source
onMissAction: alertfinally fires an actual alert into System Health. - Optional
expectedRowCountDeltaper target node emits awarningalert when today’s load is wildly outside the recent average.
id BIGSERIAL column at table creation; the
runtime has supported user-defined PKs since the DDL helper landed.
Docs: System Health ·
Loading Modes ·
File Ingestion · 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
bigintor Postgresnumeric/decimalcolumns. The lineage payload now serialises large integers as strings (preserves precision pastNumber.MAX_SAFE_INTEGER) and renders Decimal columns in their human-readable form, solifetime_valueetc. render as"10552.97"instead of internal-state JSON. See 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 atitle=tooltip for the full text. - Editor is now 2-pane — the redundant “Recent runs” left pane was
removed; the bottom strip + dedicated
/data/flow-runspage cover run history. Center pane fills the freed width. - Branch node form —
thenanddefaultroute 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/fieldswith 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 existingPOST /api/v1/connectors/test. IRarchiveNodeSchemagains an optionalconnectorIdfield (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 theexpressionop render as “preview-limited — run the pipeline” since they need server-side runtime context.
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_boostedtraining set with synthetic neighbors of marginal rows. See 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. - KernelSHAP —
POST /api/v1/decisions/:id/shapnow dispatchesgradient_boostedto TreeSHAP andneural_cfto KernelSHAP. See SHAP. - Three new fairness metrics — Gini coefficient, DeLong paired-AUC test, two-sample Kolmogorov-Smirnov. See Advanced Fairness.
- 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-purgesweeps decision traces, interaction history, and AI attachments past the strictest tenant retention setting. See 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. - 26 new connector entries added — registry expansion spanning
CRM / MAP / CDP / audience-sync / helpdesk / workflow vendors. New
entries ship as
coming_soonform-only stubs. - 4 new pipeline transforms —
vector_embed,geo_resolve,sentiment_score,language_detectwith HTTP-pointed runtime adapters underlib/flow/runtime/transforms/external-model-call.ts. - SCIM 2.0 + WebAuthn + SIEM audit-log shipping —
/scim/v2/Usersendpoints, full COSE-key parse with ES256 / RS256 assertion verification, and Splunk HEC / Datadog Logs / Elastic_bulkbackends gated by SSRF validation. - Multi-region overlay —
helm/values-multi-region.yamlchart 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 kaireonCLI, Postman v2.1 collection, and MCP marketplace manifest undersdks/. - OpenAPI auto-discovery —
tools/scripts/gen-openapi.mjswalks everyapp/api/v1/**/route.tsand emitsplatform/public/openapi.jsoncovering the full v1 surface.
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.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 for the optional
wiring path, and the roadmap for the pilot guardrails we
plan to ship before enabling automation by default.Phase 01 — Analytics Foundation
New analytical primitives that power every downstream surface in this milestone.- New
dashboard-datacaseselection_frequency— per-offereligibleCount,scoredCount,selectedCount,selectionRate,avgRank, andrankDistribution[]. AcceptschannelId,categoryId,decisionFlowId,segmentIdfilters. - New
dashboard-datacaseanomaly_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-datacasewhy_not_ranked— aggregate misses per offer:scoredTooLow,filteredByContactPolicy,filteredByQualification, andbeatenBy(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/rankAfteron scoring results. - Cross-decision narrative helpers that explain offer underperformance, segment coverage, and anomalies in plain language.
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 inchannels, and respectscooldownMinutes. - New settings page
/settings/alerts— CRUD for alert rules. - New API surface:
GET /api/v1/notifications/providersandPOST /api/v1/notifications/providersGET /api/v1/notifications/providers/:id,PATCH /api/v1/notifications/providers/:id,DELETE /api/v1/notifications/providers/:idPOST /api/v1/notifications/providers/:id/testPOST /api/v1/notifications/sendGET /api/v1/alertsandPOST /api/v1/alerts(alert rules — later renamed from/alert-rulesto/alerts)GET /api/v1/alerts/:id,PUT /api/v1/alerts/:id,DELETE /api/v1/alerts/:idPOST /api/cron/tick(token-authenticated; intended caller is an external scheduler such as AWS EventBridge).
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 insrc/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/reportsbuilder UI — compose sources, pick formats/narrative, schedule + destinations, live preview, runs history drawer./api/cron/tickextended to also process due report schedules; response JSON gainsreportsEvaluated,reportsRan,reportErrors.
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/previewunder 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).
Earlier changes
Earlier changes are tracked in commit history — see the platform repo. 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.