Handle voice drift
Configure thresholds, wire the `persona.audit_failed` webhook into your QA queue, and (optionally) enable auto-recovery so flagged replies never reach the user.
Drift is the gap between a persona's voice fingerprint and what it just said. Every chat reply carries that gap as a score. This tutorial wires the score into something operational: a tuned threshold, a webhook subscription, and an optional auto-recovery path.
1. Tune the threshold
The default engine.pipeline.drift_detection.threshold = 0.30 is a
middle-of-the-road choice. For brand-critical surfaces, tighten:
curl -X POST https://api.moonborn.co/v1/config/engine.pipeline.drift_detection.threshold \
-H "Authorization: Bearer $MOONBORN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "value": 0.20, "scope": "workspace", "scopeId": "ws_..." }'await client.config.setItem({
key: 'engine.pipeline.drift_detection.threshold',
value: 0.2,
scope: 'workspace',
scopeId: 'ws_...',
});Per-persona overrides live in the persona's runtime contract — useful when one persona in the org is a strict support agent and another is a loose creative companion.
2. Decide on an alert action
engine.pipeline.drift_detection.action_on_alert accepts:
warn(default) — reply ships, alert is logged, webhook fires.auto_recover— Moonborn regenerates once with the fingerprint re-injected, ships whichever reply scores lower.block— caller gets409 Conflictwith the drift envelope; decide downstream.
For customer support, auto_recover plus a webhook escalation is the
common pattern. For creative play, warn plus a UI badge is enough.
3. Subscribe to the webhook
persona.audit_failed fires whenever the drift threshold trips
(among other audit failures). Subscribe with POST /v1/webhooks:
const hook = await client.webhooks.createWebhook({
url: 'https://your-app.com/webhooks/moonborn',
events: ['persona.audit_failed'],
description: 'Drift escalations → CX QA queue',
});
console.log('Save this secret once:', hook.signingSecret);The signing secret is returned exactly once. Store it; subsequent
deliveries are HMAC-signed (header X-Moonborn-Signature).
4. Verify and act
import crypto from 'node:crypto';
export function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature),
);
}The payload for a drift alert looks like:
{
"id": "evt_01H...",
"type": "persona.audit_failed",
"occurredAt": "2026-05-16T12:00:00Z",
"data": {
"personaId": "persona_01H...",
"sessionId": "sess_01H...",
"messageId": "msg_01H...",
"driftScore": 0.41,
"driftThreshold": 0.30,
"reason": "voice_drift"
}
}Push this into your QA queue, your ops Slack channel, or your incident runbook — whichever pipeline your team owns.
5. Replay deliveries during outages
Webhooks retry up to five times with exponential backoff (1m → 2m → 5m → 30m → 2h), then move to the dead-letter queue. Replay manually from the dashboard or via API:
const deliveries = await client.webhooks.listDeliveries({
webhookId: hook.id,
status: 'failed',
});
for (const d of deliveries) {
await client.webhooks.replayDelivery({
webhookId: hook.id,
deliveryId: d.id,
});
}Next
- Deep-dive on the score itself: Drift detection concept.
- Sharper tuning per audience: Drift threshold tuning.
- Webhooks reference.