The AI assistant was answering questions about everyone's data at once.
Every tool method in AiChatService — all ten-plus of them — was querying without a tenant ID. Ask Bermuda "how many leads do I have?" and it would count leads across every tenant in the database. The number came back confident and wrong: 1,700 when the real answer was 58. Not a hallucination — a scope failure. The query was correct. The filter was missing.
This is a data isolation bug, and in a multi-tenant system it's the most dangerous kind. Not because it crashes — it doesn't — but because it returns plausible data. A lawn care operator sees 1,700 leads and thinks business is booming. The number is real. It's just not theirs.
Tenant ID scoping went into every tool method: pipeline summaries, revenue calculations, lead creation, customer lookups, quote calculations. The is_deleted and is_hidden filters got added to order queries so the AI's pipeline summary matches what the Leads page shows. Revenue and pipeline queries moved from get() with PHP counting to count() and sum() at the database level — faster and less likely to diverge from the source of truth.
The chat persistence fix was a separate problem. Bermuda's conversation would disappear on navigation. The AI panel was rendered with v-if, which destroys and recreates the component on every Inertia page transition. Switched to v-show, which hides it visually but keeps it alive in the DOM. For the hard-refresh edge case — where even v-show can't help because the whole page reloads — an onMounted hook reloads conversation messages from the server.
The data isolation fix is the one that matters. An AI that sees across tenant boundaries isn't just inaccurate — it's a trust violation. The operator doesn't know the data is wrong, because the data isn't obviously wrong. It's just not scoped. And unscoped data in a multi-tenant system is someone else's data wearing your name.