Skip to content

Resources · Changelog

v6.1.0 · AI assistant grounding

Released 2026-03-15 · 11 changes · 4 contributors · ~8 minute read

Stablev6.1.0ShareRSS

Headline

The assistant stops making things up, by design.

v6.1.0 rebuilds the AI assistant on a retrieval-grounded foundation. Every answer must cite a row, a setting, or a policy document. Answers that can't cite become honest “I don't know” refusals instead of confident fiction. We also shipped calendar conflict warnings and an assistant-driven invoice triage flow.

Section 1

Highlights

Three threads in this release. AI grounding was the headline; the calendar work was the second-most operator-requested feature in our feedback survey; and the edge-function cold-start cut was the quiet win that operators won't notice directly but will feel in every dashboard load.

  • Assistant grounding — every answer cites a source. Confidence gating cuts off low-evidence answers before they reach the user.
  • Calendar conflict warnings— surfaces overlaps proactively as you draft a booking. Includes a one-click “resolve by shifting the new booking” option.
  • Edge function cold-start cut — median cold start down 22% after deferring Sentry init for fast-path requests.
Assistant answer card with inline citations — every claim links back to the row that supports it.

Section 2

AI assistant

The pre-6.1.0 assistant was a thin shim over an LLM. It was fast and friendly, and occasionally invented things — a non-existent member, a not-yet-shipped feature, a member tier that the tenant didn't have. The new assistant is retrieval-grounded from the ground up.

How grounding works

On every turn, the assistant first retrieves up to 8 candidate sources from the tenant's data: rows from members, bookings, invoices, and the tenant's operator-curated knowledge base. The candidate set is RLS-scoped, so the assistant cannot see rows the asker couldn't see directly. The model is then prompted to answer using only those sources or refuse.

apps/assistant/src/grounding.ts (excerpt)
ts
// Refusal is a first-class response, not an error.
type GroundedAnswer =
  | { kind: 'grounded'; text: string; citations: Citation[] }
  | { kind: 'refused'; reason: 'no_evidence' | 'out_of_scope' }

async function answer(question: string, ctx: AskerContext): Promise<GroundedAnswer> {
  const candidates = await retrieve(question, ctx, { limit: 8 })
  if (candidates.length === 0) {
    return { kind: 'refused', reason: 'no_evidence' }
  }
  const llmAnswer = await callModel({ question, candidates, ctx })
  if (llmAnswer.confidence < 0.7) {
    return { kind: 'refused', reason: 'no_evidence' }
  }
  return { kind: 'grounded', text: llmAnswer.text, citations: llmAnswer.citations }
}

What “refused” looks like

When the assistant can't find evidence, the user sees a short, honest line: “I couldn't find anything that answers this. Want me to search a different way?” This is intentionally not a generic empty state — the assistant offers concrete alternatives based on partial matches.

Refusal card with three alternative-search suggestions — one for member name, one for booking date, one for the curated KB.

Citations

Every claim is linked to a source. Clicking a citation opens a panel with the underlying row or document, scoped to the asker's permissions. Citations are retrievable via the API for downstream auditing — every assistant turn is logged with its citation set for 90 days.

Section 3

Calendar

Calendar conflict warnings were the most-requested feature in our Q4 2025 feedback survey. They're straightforward — when the slot you're drafting overlaps an existing booking, we tell you immediately, with a one-click resolution.

  • Inline warning banner on the booking draft sheet, including a shortcut to view the overlap.
  • Shift-to-fit button suggests the nearest non-overlapping slot (15 / 30 / 60 minute snap, configurable per tenant).
  • Recurring booking conflicts are surfaced at draft-time, not at save-time. The list shows which occurrences are affected.
conflict detection (excerpt)
sql
-- The conflict probe runs in the same tx as the draft save.
SELECT b.id, b.start_at, b.end_at, b.member_id
  FROM public.bookings b
 WHERE b.resource_id = $1
   AND b.status IN ('confirmed','pending_payment')
   AND tstzrange(b.start_at, b.end_at, '[)')
       && tstzrange($2::timestamptz, $3::timestamptz, '[)');
Draft booking sheet — the conflict banner now appears live as the user picks an end time.

Section 4

Performance

  • Edge function cold-start: down 22% on the median (was 320ms, now 250ms). The win came from deferring Sentry init for fast-path requests.
  • Member portal home: down 18% in JS bundle size after we moved the avatar generator behind a dynamic import.
  • Assistant turn time: p50 down from 1,800ms to 1,250ms (~30%) after we parallelised retrieval with the model warmup.
  • Booking list query: 11% faster on the median tenant after we added a covering index on (resource_id, status, start_at).
Edge function cold-start, median across 14 days
text
Before v6.1.0           ~ 320ms
After v6.1.0            ~ 250ms      ⟶  -22%

Notable: the p95 saw a smaller gain (-9%) because the deferred-Sentry
win is most visible on requests that don't need the full SDK loaded.

Section 5

Fixes & polish

  • Assistant follow-up loop — a multi-turn assistant conversation could rarely lose the tenant context between turns. The conversation context is now persisted server-side keyed by the conversation id, not the client session.
  • Calendar drag-to-create on touch devices used to occasionally double-fire on rapid taps. Debounced at the pointer-event layer.
  • Invoice PDF font subset— non-ASCII characters (café, résumé) used to render as blanks. Now embeds the Latin-1 + Latin-Ext subset by default.
  • Member portal dark mode contrast — three components had insufficient contrast in dark mode. Failed our internal Axe audit. Fixed.

Section 6

Migration notes

One operator-facing note, one for integrators.

1. AI assistant feature flag

The new grounded assistant is on by default for all tenants. The previous, ungrounded version is removed — there is no fallback flag. If you had your own feature flag on for the old assistant, you can delete it; the new one is always on.

2. Webhook payload: turn objects

The assistant.turn.completed webhook payload gained two new fields: kind (grounded | refused) and citations (array of { type, id, snippet }). Old fields are unchanged. If you have a typed schema validator, expect the new fields to start arriving on 2026-03-15.

$ curl -X POST $WEBHOOK_URL
bash
// assistant.turn.completed payload (shape, abbreviated)
{
  "event": "assistant.turn.completed",
  "turn_id": "turn_01HXY...",
  "conversation_id": "conv_01HXY...",
  "asker_user_id": "usr_01HXY...",
  "kind": "grounded",                   // NEW
  "citations": [                          // NEW
    { "type": "booking", "id": "bkg_01...", "snippet": "..." }
  ],
  "latency_ms": 1180
}

Privacy note: Citation snippets are scoped to what the asker could already see. The webhook receiver can therefore include them in operator-facing audit dashboards without additional permission checks.

Found this useful?

Share the release notes with your team.

One short link per release, hosted publicly. No login wall.