Open app
Moonborn — Developers

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 gets 409 Conflict with 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