> ## Documentation Index
> Fetch the complete documentation index at: https://docs.drdj.me/llms.txt
> Use this file to discover all available pages before exploring further.

# In-App AI Copilot

> The self-hosted CopilotKit chat assistant embedded in the web app: multi-provider models, free-by-default OpenRouter rotation, human-in-the-loop trust model, and per-user self-learning.

The in-app AI copilot is a chat popup inside the DealDash web app. It acts **as the signed-in user**: every action it takes goes through the app's own `/api/*` routes with that user's JWT, so reads and writes are scoped, audited, and validated exactly like a request from the SPA.

It is a **separate path** from the external MCP agent bridge at `/api/agent/*` (which serves out-of-app agents with its own OAuth and Telegram approval tiers). The copilot does not use the MCP transport, agent OAuth, or the agent-bridge approval-tier middleware.

## What It Is

* A self-hosted [CopilotKit](https://copilotkit.ai) runtime served at `/api/copilot` (Express, `server/routes/copilot.ts`).
* A per-request `BuiltInAgent` built for the signed-in user. It resolves the Supabase JWT to the local user, then calls the app's own REST routes over loopback.
* **44 server tools across 12 families**: deals, tasks, contacts, influencers, activity, suggestions, context, templates, memory, links, screenshots, and social sessions.
* Server tools are **reads plus safe low-risk writes only**. Anything sensitive runs as a frontend action gated by human approval (see below).
* Tool results are size-capped before they re-enter the prompt on later steps (`server/lib/copilot/capResult.ts`, `COPILOT_TOOL_RESULT_MAX_CHARS`, default 16 KB) — a large search/list is trimmed to its first records with a `_truncated` marker so input tokens stay bounded.

Gating:

* The server endpoint returns a clean **503** when no model provider is configured at all (`isCopilotConfigured()` checks for any of the Anthropic / OpenAI / Google / OpenRouter keys or the Hermes base URL).
* The client surface is controlled by `VITE_COPILOT_ENABLED`: production builds show the copilot unless the flag is explicitly `false`; local/dev builds remain opt-in with `VITE_COPILOT_ENABLED=true`.
* CopilotKit telemetry is disabled by default (`COPILOTKIT_TELEMETRY_DISABLED`).

## Chat UI & Discoverability

The chat is a `CopilotPopup` (`client/src/components/copilot/CopilotProvider.tsx`) with a branded theme (`copilot-theme.css`) and a custom header offering five controls:

* **What can I ask?** (grid) — opens the **Capabilities sheet** (`CopilotCapabilitiesSheet.tsx`), an organized launcher with one section per tool family (pipeline & deals, views/LinkShot, contacts, influencers, tasks, screenshots, social sessions, links, memory, templates) and curated tap-to-send starter prompts. This makes the full tool surface discoverable instead of relying only on the dynamic suggestion chips.
* **New chat** — starts a fresh thread.
* **Conversations** (history) — the threads panel (`ConversationHistorySheet.tsx`): list, resume, rename, archive, delete, and search past conversations.
* **Settings** (gear) — the **Settings sheet** (`CopilotSettingsSheet.tsx`) surfaces the provider / model / BYOK card right inside the assistant (admins also get a shortcut to the MCP-server registry).
* **Close**.

Other UI features: route-aware LLM **suggestion chips** (`CopilotSuggestions.tsx`), image **attachments** (uploaded to DealDash screenshot storage, passed to the vision model as a URL), in-chat **ApprovalCards** for sensitive actions, and **thread persistence** (each turn is saved server-side and re-hydrated on reload). All surfaces are responsive — a docked popup on desktop, a full-height sheet above the mobile nav.

## Reliability & Resource Safety

The copilot streams model output over Server-Sent Events. A stalled stream that holds the response open past Cloudflare's \~100 s edge timeout produces a `524`, which makes CopilotKit retry-storm — and that storm can exhaust the database connection pool and take the whole site down. Several bounds prevent this; **all default to safe values** and are env-tunable:

| Concern                                               | Mechanism                                                                                                                                                                                                          | Env knob (default)                                                                                            |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| A turn never holds the response past the edge timeout | Provider-agnostic request deadline ends the response + aborts the run (`server/lib/copilot/requestDeadline.ts`). This is the **only** upstream bound on the paid model path, which CopilotKit resolves internally. | `COPILOT_REQUEST_DEADLINE_MS` (90000, \< CF 100 s)                                                            |
| A tool's loopback `/api` call can't hang forever      | `AbortSignal.timeout` on the loopback fetch (`api.ts`) → surfaces a structured 504 the model can react to.                                                                                                         | `COPILOT_LOOPBACK_TIMEOUT_MS` (25000)                                                                         |
| Free-rotate stalls fail over fast                     | Per-attempt connect / first-token / stream-stall + total-deadline timeouts (`freeRotation.ts`). Only active on the OpenRouter free path.                                                                           | — (constants under CF 100 s)                                                                                  |
| The connection pool isn't tiny                        | `DB_POOL_SIZE` raised from 8 → 20; fast `connect_timeout`.                                                                                                                                                         | `DB_POOL_SIZE` (20), `DB_CONNECT_TIMEOUT_S` (10)                                                              |
| No single query/transaction pins a pool connection    | Optional, pooler-safe-by-default Postgres backstops (off unless set; recommended at the DB **role** level too).                                                                                                    | `DB_STATEMENT_TIMEOUT_MS`, `DB_IDLE_TX_TIMEOUT_MS`                                                            |
| Background jobs can't wedge on a hung query           | `withTimeout` races a deadline so the job loop's `finally` always runs (`server/jobs/withTimeout.ts`) — reminderChecker, syncQueueProcessor, twoWaySyncProcessor.                                                  | `REMINDER_CHECK_TIMEOUT_MS` (30000), `SYNC_QUEUE_TIMEOUT_MS` (240000), `TWO_WAY_SYNC_USER_TIMEOUT_MS` (60000) |

**Production policy:** run a **paid** model (never `free-rotate`) on prod, since the free-rotate timeouts don't cover the paid path — the request deadline above is what protects it. The mechanism is covered by `tests/integration/copilot-pool-loadtest.test.ts` (50 concurrent hung calls bound out while a healthy route stays responsive; an overloaded pool starves without reclaim and recovers with it).

## Multi-Provider Model Backend

The copilot's LLM is pluggable **per user**. Each user picks a provider and model in the **/settings → "AI Copilot Model"** card, or from the **gear icon in the chat header** (the same card, surfaced in a sheet).

Supported providers:

* Anthropic, OpenAI, Google — resolve as model strings (CopilotKit built-ins).
* OpenRouter and self-hosted Hermes / OpenAI-compatible endpoints — resolve as AI SDK model instances (`BuiltInAgent` accepts these directly).

Credentials come from two sources:

* **Server keys**, role/premium-gated via `COPILOT_SERVER_KEY_ROLES`.
* **Encrypted per-user BYOK** (bring-your-own-key), stored AES-256-GCM at rest.

Each user's choice lives in the userId-scoped `copilot_provider_config` table (RLS-protected), managed via `GET`/`PUT /api/copilot/provider-config`.

**Egress consent:** sending CRM data to a non-default third-party provider requires per-user, per-provider consent. This is **default-deny** — data does not leave to a new provider until the user explicitly consents.

## OpenRouter Catalog + Free Rotation

A curated OpenRouter catalog (`server/lib/copilot/modelCatalog.ts`) is served to the settings UI as a grouped model picker. It includes top picks for tool-heavy CRM work, best-in-class Anthropic / OpenAI / Gemini / DeepSeek models, and a free pool.

The `free-rotate` sentinel rotates across the tool-capable free models in `FREE_ROTATION_POOL`, failing over to the next model on **any non-2xx** response (free endpoints flap with 404/429), with a short per-model cooldown (`freeRotation.ts`). This keeps the copilot running for free.

**Paid models degrade instead of hard-failing:** when a paid OpenRouter model on the server key hits the key's monthly limit or runs out of credits (402, or a 403 whose body says so), the request is rewritten to the free pool and handed to the same rotation (`createPaidToFreeFallbackFetch`). Moderation 403s and other errors pass through untouched, and every degradation is persisted as a rotation event so it shows up in the admin observability view.

**Role-gating rules:**

* **Free models are exempt from role-gating** (zero cost) — any user can use them.
* **Paid models** on the server key still require an eligible `COPILOT_SERVER_KEY_ROLES` role or the user's own BYOK key. A base user cannot bill the operator's paid balance.

**Free-by-default for everyone:** the operator sets `COPILOT_PROVIDER=openrouter` + `COPILOT_MODEL=free-rotate` (the consented baseline) to run the copilot free out of the box.

Verify free tool-calling against the live API:

```bash theme={null}
node scripts/verify-openrouter-free.mjs   # ✅ TOOLS / ⚠️ TEXT-ONLY / ❌ ERROR per free model
```

This is a manual operator tool, not run in CI.

## Human-in-the-Loop Trust Model

Frontend writes are CopilotKit actions (`client/src/components/copilot/`). The trust tiers mirror the agent-bridge model:

* **Low-risk writes auto-execute** (no approval): create/complete/reopen tasks, upsert contact/influencer, add notes, add/remove labels, link a screenshot to a deal, create a short link, save memory.
* **Sensitive writes and ALL deletes require an in-chat `ApprovalCard`** (`renderAndWaitForResponse`): create/update deal, bulk view-check import, payment add/update/delete, and every delete (link, screenshot, note).

Server tools are reads plus safe low-risk writes only — the sensitive operations live on the frontend behind the approval gate.

## Self-Learning (Per-User)

The copilot learns per user across sessions. This is **not** a native CopilotKit feature — it is built on the existing `agent_memories` table.

The loop:

1. HITL approve/cancel of sensitive actions emits a signal to `agent_learning_signals` (`POST /api/copilot/feedback`, fire-and-forget from the approval cards).
2. A repeated pattern (≥3 of the same action + outcome) **graduates** into a durable `agent_memories` row (`sourceType='learned'`, server-templated content — never free text, so it is injection-safe).
3. `buildDealDashAgent` **injects** the top active learnings (userId-scoped, character-budgeted, labeled non-authoritative) into the system prompt each session.
4. Stale learned memories decay/archive after 60 days unreinforced (`curateLearnings`, throttled).

Strictly per-user — there is no cross-tenant learning. The explicit `save_memory` / `list_memories` tools feed the same injection path. Logic lives in `server/lib/copilot/learning.ts`.

## Codex (Experimental, Off by Default)

A Codex / ChatGPT-subscription provider is available behind the `COPILOT_CODEX_EXPERIMENTAL` flag (default `false`). When the flag is unset, Codex is absent from the catalog, rejected on save, and hidden in the settings card — zero behavior change.

It is **unofficial / ToS-grey**: it drives the copilot via the private `chatgpt.com/backend-api/codex/responses` endpoint using your **personal** ChatGPT/Codex subscription. There is no API key — you "Connect your Codex login" by pasting `~/.codex/auth.json` (run `codex login` locally first). The server parses it into a normalized token blob, stores **only that blob, encrypted** at rest (never the raw paste, never returned to the client), and self-refreshes the access token before each request (`server/lib/copilot/codex.ts`). Egress consent is still required (Codex is non-default).

**Caveat — tool calls are UNVERIFIED.** The private endpoint may strip the `tools` array, which would make Codex unusable for the tool-driven copilot. This cannot be confirmed without a real token. Before trusting Codex, run the make-or-break gate:

```bash theme={null}
node scripts/verify-codex-tools.mjs ~/.codex/auth.json   # ✅ tools honored / ❌ tools stripped
```

This is a manual operator tool, not run in CI.

## Admin Controls

The copilot has an operator/admin surface that acts **across users** — a distinct third concern from `/api/copilot` (per-user, acts-as-the-signed-in-user) and `/api/agent/*` (the external MCP bridge). It is admin-only and reads are deliberately separate from the per-user query layer so the privilege boundary is explicit.

**Dashboard:** `/admin/copilot` (`client/src/pages/admin/Copilot.tsx`, admin-only via `AdminRoute`, linked from the admin nav). Tabs:

* **Overview** — usage headline, learning aggregates, free-rotation snapshot, and the free-tier cap.
* **Usage** — paginated per-user FREE-model usage rollups.
* **Providers** — every user's provider/model, BYOK presence (boolean only), and egress consent — all secrets redacted.
* **Learning** — self-learning signal and graduated-memory aggregates across users (CRM data is only counted, never surfaced).
* **Observability** — free-rotation failovers, per-model aggregates, and live cooldowns.

**Admin endpoints** (`server/routes/adminCopilot.ts`, mounted at `/api/admin/copilot` with `requireAuth` + `requireAdmin`, ahead of the generic `/api/admin`):

| Method   | Path                                         | Purpose                                                                    |
| -------- | -------------------------------------------- | -------------------------------------------------------------------------- |
| `GET`    | `/api/admin/copilot/overview`                | usage headline + learning aggregates + rotation snapshot + free-cap        |
| `GET`    | `/api/admin/copilot/usage`                   | paginated per-user free-model usage rollups (`?from&to&search&page&limit`) |
| `GET`    | `/api/admin/copilot/provider-configs`        | paginated provider/model/BYOK-presence/consent (REDACTED)                  |
| `GET`    | `/api/admin/copilot/learning`                | self-learning signal + graduated-memory aggregates across users            |
| `GET`    | `/api/admin/copilot/rotation`                | free-rotation failovers + per-model aggregates + live cooldowns            |
| `GET`    | `/api/admin/copilot/settings`                | the copilot free-tier daily cap                                            |
| `PUT`    | `/api/admin/copilot/settings`                | set `copilotFreeDailyLimit` (`0` = unlimited; authoritative over env)      |
| `GET`    | `/api/admin/copilot/mcp-servers`             | list global MCP servers; auth headers redacted to `hasAuthHeader`          |
| `POST`   | `/api/admin/copilot/mcp-servers`             | register an SSRF-guarded global MCP server; auth header encrypted          |
| `PUT`    | `/api/admin/copilot/mcp-servers/:id`         | update a registered MCP server                                             |
| `PATCH`  | `/api/admin/copilot/mcp-servers/:id/enabled` | enable or disable a registered MCP server                                  |
| `DELETE` | `/api/admin/copilot/mcp-servers/:id`         | remove a registered MCP server                                             |

GBrain project memory for the in-app copilot is a separate server-side concern from local Claude/Codex stdio MCP config. Operators can expose it in either of two ways:

* Register a public HTTP/SSE GBrain MCP endpoint in the admin MCP registry.
* Set `COPILOT_GBRAIN_MCP_URL` with optional `COPILOT_GBRAIN_MCP_TRANSPORT=http|sse` and `COPILOT_GBRAIN_MCP_AUTH_HEADER`.

Use HTTP when the endpoint needs a static auth header; unauthenticated/public SSE is allowed. The env-backed GBrain endpoint is loaded before DB-registered MCP rows, skips duplicate DB rows for the same URL, and remains available if the registry query fails. It uses the same public-URL SSRF guard and header validation as admin-registered MCP servers; do not put credentials in the URL.

Production should also keep `DEALDASH_GBRAIN_REQUIRE_CLOUD=true` with a dedicated `DEALDASH_GBRAIN_DATABASE_URL`. The runtime `/api/readiness` probe reads GBrain `config.version` when cloud GBrain is required, so deployment health catches a missing or unhealthy project-memory database.

The cross-user query layer (`server/lib/copilot/admin.ts`) is intentionally separate from the per-user functions in `usage.ts` / `providerConfig.ts` / `learning.ts`. BYOK key ciphertext is never selected or exposed (only `hasByokKey: boolean`); learning `proposedArgs` (user CRM data) is only ever counted, never surfaced verbatim.

**Admin-settable free cap.** The free-tier daily limit is set in the dashboard and persisted in the global `app_settings` table. `usage.resolveFreeDailyLimit()` makes the admin `copilotFreeDailyLimit` win (an explicit `0` means unlimited) over the `COPILOT_FREE_DAILY_LIMIT` env, which is only the fallback when an admin has never set it.

**System settings + enforcement.** The admin Settings page (`GET`/`PUT /api/admin/settings`) now really hydrates and persists its system toggles (also backed by `app_settings`):

* **Maintenance mode** (`server/middleware/maintenance.ts`) blocks non-admin `/api` traffic when on — exempting `/api/auth`, `/api/admin`, and health — and is fail-open. Admins always pass through.
* **Public signup** (`allowPublicSignup`) is enforced in `resolveUserFromToken`: when off, it blocks NEW account creation while leaving EXISTING users unaffected (fail-open).

Persisted free-rotation failover events (`copilot_rotation_events`) and the `app_settings` table are both **admin-only** (RLS enabled, deny-by-default with no permissive policy); the server's RLS-bypassing connection serves them through the `requireAdmin`-gated routes only.

## Security

* **Identity always comes from the JWT.** Tools call `/api/*` over loopback with the signed-in user's token, so every read/write is scoped and audited like a normal request.
* **Provider keys are encrypted at rest** (AES-256-GCM), **never returned to the client**, and **never reach the model**. Provider error bodies are redacted before reaching the model (`tool.ts`) so a provider 401 cannot leak a key.
* **Strict per-user isolation.** Provider config, BYOK keys, and learned memories are all userId-scoped with RLS; there is no cross-tenant access or learning.
* **Admin reads are an explicit, `requireAdmin`-gated exception** to the per-user RLS default — operator-across-users visibility, with BYOK key ciphertext never selected and all secrets redacted.

## QA Status

The live-LLM QA for the copilot is **operator-run**, not automated in CI. Do not assume an automated pass. The manual QA handoff is in `docs/superpowers/plans/2026-06-13-copilot-QA-checklist.md`.

## Source of Truth

* `README_FOR_AI.md` — the "In-app AI copilot" sections (authoritative current description).
* `docs/superpowers/plans/2026-06-13-copilot-*.md` — the implementation plans: `multiprovider-foundation`, `openrouter-catalog`, `self-learning`, `codex-experimental`, `settings-ui`, `isolation-suite`, and `QA-checklist`.
