PLAN_SMART_INBOX.md27.0 KBView on GitHub
# Smart Inbox Design Plan

## Current State

- **Labels**: Fully dependent on Gmail/IMAP. No internal label table. AOP labels are written to Gmail as `[Cedar]/aop/{aopId}` hidden labels via `syncAopLabel()`.
- **Right sidebar**: Shows `EmbeddedCedarChat` when no thread is open; `ConversationContext` when a thread is open.
- **Inbox navigation**: Static folder list (inbox, sent, drafts, archive, spam, snoozed, bin) + conditional "Deals" folder if a Deals AOP exists. All backed by Gmail system labels.
- **AOP linking**: `crmConversations.aopId` → Gmail label `[Cedar]/aop/{aopId}`. Backfill available.
- **Calendar**: `listEvents()` tRPC route exists; no dedicated "next event" query.
- **Email classification**: `crmEmailEvents.classification` stores inbound/outbound type. No importance classification.
- **AOPs**: No `icon` column. Color only.

---

## Proposed Changes

### Phase 0 — Layout Refactor: Static Sidebar + Next Event Panel

**Goal**: Replace the default right-sidebar chat with a persistent contextual widget (next meeting + inbox nav). Sidebar becomes static — no embedded Cedar chat in mail routes.

#### 0.1 — `selectNextEvent` Zustand selector

**File**: `apps/mail/modules/calendar/store/calendarSlice.ts`

No new tRPC route needed. `CalendarDataSync` (mounted globally in `hotkey-provider-wrapper.tsx`) already fetches a rolling ±2-week window of events and syncs them into `calendarEvents` in the Zustand store.

Add a selector alongside the existing `selectEventsForDay`:

```ts
export const selectNextEvent = (state: { calendarEvents: CalendarEventWithCalendarId[] }) => {
  const now = new Date();
  return (
    state.calendarEvents
      .filter((e) => {
        const raw = e.start?.dateTime || e.start?.date;
        return raw ? new Date(raw) > now : false;
      })
      .sort((a, b) => {
        const aStart = a.start?.dateTime || a.start?.date || '';
        const bStart = b.start?.dateTime || b.start?.date || '';
        return new Date(aStart).getTime() - new Date(bStart).getTime();
      })[0] ?? null
  );
};
```

Usage in components: `const nextEvent = useCedarStore(selectNextEvent);`

#### 0.2 — Right Sidebar: NextEventWidget

Replace the default `EmbeddedCedarChat` content with a new `NextEventWidget` component:

```
┌─────────────────────────────┐
│  Next Meeting — in 36 min   │
│  Standup                    │
│  3:00–3:30pm                │
│  👤 Alice, Bob, Carol       │
│  [Meeting Prep]  [Join]     │
└─────────────────────────────┘
```

- **"Meeting Prep"** button: opens the conversation linked to that calendar event (via `crmConversations` where `calendarEventId` matches), pushing it onto the `viewStack`.
- **"Join"** button: opens `meetUrl` in a new tab. Only rendered if `meetUrl` is present.
- If no upcoming event: render a minimal empty state ("No upcoming meetings").

**New files**: `apps/mail/components/mail/NextEventWidget.tsx`, `apps/mail/components/mail/InboxNavWidget.tsx`, `apps/mail/components/mail/MailSidebarWidgets.tsx`

#### 0.3 — Right Sidebar: InboxNavWidget

Below the next-event card, render the full inbox folder list (the same set currently shown in the left folder nav):

```
─────────────────
  ○ Inbox         (12)
  ○ Smart Inbox
  ○ Done
  ○ All Mail
  ○ Deals
  ○ Drafts
  ○ Sent
  ○ Snoozed
  ○ Archive
  ○ Spam
  ○ Bin
─────────────────
```

- Each item navigates to the corresponding route (`/mail/inbox`, `/mail/smart-inbox`, etc.).
- Badge counts sourced from the existing `useStats()` hook.
- "Smart Inbox" and "Done" entries added here (grayed/disabled until Phase 2 is complete).
- "All Mail" entry links to `/mail/all`.

**New file**: `apps/mail/components/mail/InboxNavWidget.tsx`

#### 0.4 — Static Sidebar (no embedded chat)

- Add `'mail-widgets'` to `Sidepanel`'s `defaultContent` union type.
- When `defaultContent === 'mail-widgets'`, skip `SidepanelChatOverlay` entirely: render `MailSidebarWidgets` by default, and `ConversationContext` / `CalendarSidebar` / `DateHighlightSidebar` when the stack drives it.
- `EmbeddedCedarChat` is not rendered at all in this mode.
- Update `apps/mail/app/(routes)/mail/layout.tsx` to pass `defaultContent='mail-widgets'`.

---

### Phase 1 — Internal Label System

**Goal**: Build a first-class internal label store in Postgres. All Cedar-managed labels (AOP assignment, importance, future tags) live here — nothing is pushed to Gmail unless the user explicitly opts in.

#### 1.1 — DB Schema

**Package**: `packages/db/`

New tables:

```sql
-- Unified label table — covers both Gmail-synced labels and Cedar-native labels.
-- `source` distinguishes ownership: Gmail owns 'gmail' rows, Cedar owns 'cedar' rows.
CREATE TABLE labels (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id           TEXT NOT NULL,
  name              TEXT NOT NULL,           -- e.g. "aop/deals", "priority/important"
  display_name      TEXT NOT NULL,           -- human-readable
  color             TEXT,                    -- hex or named colour
  icon              TEXT,                    -- lucide icon name or emoji
  system            BOOLEAN DEFAULT FALSE,   -- TRUE = Cedar-managed, not user-deletable

  -- 'gmail'  = sourced from Gmail API (Cedar is a cache, Gmail is authoritative)
  -- 'cedar'  = Cedar-native, not pushed to Gmail unless the user opts in
  source            TEXT NOT NULL DEFAULT 'cedar',

  -- Populated when source='gmail', or when a 'cedar' label has been pushed to Gmail.
  -- NULL = never pushed to Gmail.
  gmail_label_id    TEXT,

  -- NULL = not applicable; 'pending' = queued for Gmail push; 'synced' = live in Gmail
  gmail_sync_status TEXT,

  created_at        TIMESTAMPTZ DEFAULT now(),
  UNIQUE (user_id, name)
);

-- Thread ↔ label associations (unified — replaces Gmail label cache + any future internal store)
CREATE TABLE thread_labels (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  thread_id  TEXT NOT NULL,
  label_id   UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
  user_id    TEXT NOT NULL,
  applied_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE (thread_id, label_id, user_id)
);

-- AOP icon column (alter existing)
ALTER TABLE agent_operating_procedures ADD COLUMN icon TEXT;
ALTER TABLE org_aops ADD COLUMN icon TEXT;
```

**Sync rule**: when re-syncing from Gmail, only update rows where `source = 'gmail'`. Cedar-owned rows (`source = 'cedar'`) are never overwritten by a Gmail sync.

**"Push to Gmail" flow** (user opts a Cedar label into Gmail):
1. User opts in → set `gmail_sync_status = 'pending'`
2. Sync job calls `labels.create` on the Gmail API → stores `gmail_label_id`, sets `gmail_sync_status = 'synced'`
3. `syncAopLabel()` checks `gmail_sync_status` before pushing; skips if `NULL` or `'cedar'`-source with no opt-in

System labels seeded on first login / migration:
| name | display_name | system | source |
|---|---|---|---|
| `priority/important` | Important | true | cedar |
| `priority/not-important` | Not Important | true | cedar |
| `inbox/active` | Active | true | cedar |
| `inbox/done` | Done | true | cedar |

**Inbox state semantics**: every thread in `INBOX` is implicitly `inbox/active` until the user marks it done. `inbox/done` is the Superhuman-style "processed" state — the thread is removed from all active inbox views but remains searchable and visible in a dedicated "Done" folder. Marking done does **not** archive to Gmail; it is a Cedar-internal read state. Undoing done re-applies `inbox/active`.

AOP labels are created dynamically in `labels` when an AOP is created, with `name = 'aop/{aopId}'` and `source = 'cedar'`.

#### 1.2 — tRPC Routes

**File**: `apps/server/src/trpc/routes/labels.ts` (new)

Procedures:
- `list` — all labels for current user (system + AOP + user-created; optionally filter by `source`)
- `create` — user creates a Cedar-native label (name, display_name, color, icon)
- `update` — rename, recolor, change icon
- `delete` — soft-delete (only non-system labels)
- `applyToThread(threadId, labelId)` — insert into `thread_labels`
- `removeFromThread(threadId, labelId)` — delete from `thread_labels`
- `getThreadLabels(threadId)` — all labels for a thread
- `listThreadsByLabel(labelId, pagination)` — threads with a given label (for folder-style views)
- `requestGmailSync(labelId)` — set `gmail_sync_status = 'pending'` to opt a Cedar label into Gmail

#### 1.3 — AOP Label Migration

**Current**: `syncAopLabel()` writes `[Cedar]/aop/{aopId}` to Gmail.  
**Target**: Write to `internal_thread_labels` instead. Gmail push removed.

**Files to change**:
- `apps/server/src/services/` — wherever `syncAopLabel()` is called
- `apps/server/src/trpc/routes/` — `backfillAopEmailLabels` route
- Remove `buildCedarAopLabelName()` usages (or keep only for legacy read-back)

Migration path:
1. On deploy, run a one-time backfill: for every `crmConversations` row with an `aopId`, insert into `internal_thread_labels` with the corresponding `aop/{aopId}` label.
2. Remove Gmail label push from `syncAopLabel()`. Keep Gmail label *read* temporarily (for legacy threads) with a feature flag `INTERNAL_LABELS_ENABLED`.
3. Remove the feature flag after a soak period.

#### 1.4 — Frontend Label UI

- Existing folder nav "Deals" tab: now backed by `listThreadsByLabel('aop/{dealsAopId}')` instead of Gmail label query.
- Thread row: render label chips for internal labels (excluding system labels from raw display).
- AOP selector: shows `icon` field if set (from Phase 1.1 schema change).

---

### Phase 2 — Smart Inbox

**Goal**: A first-class inbox view that splits threads into "Priority" and "Other" sections, driven by an agent classification stored as an internal label.

#### 2.1 — Agent Classification

**What the agent classifies**: every inbound thread that lands in `INBOX`.

**Output**: one of two internal labels: `priority/important` or `priority/not-important`.

**Trigger**: existing agent workflow hook — on new thread sync (same point where AOP selection runs today).

**Classification logic** (prompt guidance for the agent skill):
- Important: direct requests requiring action, replies from existing contacts, flagged threads, threads from VIP senders (based on past interaction frequency).
- Not important: newsletters, automated notifications, CC'd threads with no action required, bulk mail.

**Storage**: `thread_labels` with `label_id` = the importance label's UUID.

**File**: `apps/server/src/services/enrichment/` — new skill or extend existing classification skill.  
Adds a step after AOP assignment: if AOP assigns to inbox (not a deals/support AOP that handles it), run importance classification.

#### 2.2 — Smart Inbox Route

**New route**: `apps/mail/app/(routes)/mail/smart-inbox/`

- Mirrors the structure of `apps/mail/app/(routes)/mail/[folder]/`.
- **Only shows `inbox/active` threads** — `inbox/done` threads are excluded entirely.
- Fetches threads in two separate queries (both filtered to `inbox/active`):
  - `listThreadsByLabel('priority/important')` → "Priority" section
  - `listThreadsByLabel('priority/not-important')` → "Other" section
- Each section is a standard `MailList` with a section header divider.
- Unclassified threads (not yet labelled) appear in Priority by default with a subtle "unreviewed" indicator.

```
Smart Inbox
──────────────────────
▼ Priority            (8)
  [thread row]
  [thread row]
  ...

▼ Other              (24)
  [thread row]
  [thread row]
  ...
```

#### 2.3b — Done State (Superhuman-style)

**Keyboard shortcut**: `e` on a focused thread → marks it `inbox/done`, removes from Smart Inbox immediately (optimistic update). An undo toast appears for ~5 seconds.

**Done folder**: `/mail/done` — a read-only view backed by `listThreadsByLabel('inbox/done')`. Shows all processed threads in reverse-applied order. Same two-section layout (Priority / Other) so importance classification is still visible.

**Marking active again**: from the Done folder, pressing `e` (or a "Move to Inbox" context menu action) swaps `inbox/done` → `inbox/active`, making the thread reappear in Smart Inbox.

**New tRPC helpers** (added to `labels.ts`):
- `markDone(threadId)` — atomically removes `inbox/active`, applies `inbox/done`
- `markActive(threadId)` — atomically removes `inbox/done`, applies `inbox/active`

**Standard inbox**: the classic `/mail/inbox` Gmail-backed view is unaffected by `inbox/done` — this state only filters the Cedar Smart Inbox and Done views. Users who never open Smart Inbox see no change.

#### 2.3c — All Mail Route

**New route**: `/mail/all`

Mirrors Gmail's "All Mail" — every thread the user has ever sent or received, regardless of folder, label, or done-state. No filtering by `inbox/active`/`inbox/done`; no folder label filter. Essentially a flat, reverse-chronological list of all threads.

- Backed by the existing thread list query with **no label filter** (or a wildcard that excludes nothing).
- Respects the same search/sort controls as other mail views.
- Useful as an escape hatch: if a thread disappears from Smart Inbox (marked done) or from Inbox (archived), it is always findable in All Mail.
- No section headers (no priority split) — just a single flat list.

#### 2.3 — User Overrides

- Right-click thread → "Mark as Important" / "Mark as Not Important" — calls `applyToThread` / `removeFromThread` on the importance label, updates the opposing label.
- Override is persisted in `thread_labels`. Agent does not overwrite user overrides (a `user_override` boolean column, or simply: if the label was applied after the agent's timestamp, treat it as canonical).

#### 2.4 — AOP Icon Column

- `agent_operating_procedures.icon` (added in Phase 1.1) exposed in:
  - AOP list/edit UI — icon picker (lucide icon name or emoji input)
  - Thread row: small icon badge next to the AOP color dot
  - Right sidebar `InboxNavWidget`: AOP folder entries show icon

---

## Critical Files

| Area | Files |
|---|---|
| DB schema | `packages/db/schema/` (new `labels`, `thread_labels` tables; alter `agent_operating_procedures`) |
| Labels API | `apps/server/src/trpc/routes/labels.ts` (new) |
| Calendar next-event | `apps/server/src/trpc/routes/calendar.ts` |
| AOP label sync | `apps/server/src/services/` — `syncAopLabel()` and callers |
| Importance classification | `apps/server/src/services/enrichment/` |
| Sidepanel | `apps/mail/components/ui/sidepanel.tsx`, `SidePanelContainer.tsx` |
| New widgets | `apps/mail/components/mail/NextEventWidget.tsx`, `InboxNavWidget.tsx` (new) |
| Smart inbox route | `apps/mail/app/(routes)/mail/smart-inbox/` (new) |
| Done route | `apps/mail/app/(routes)/mail/done/` (new) |
| All Mail route | `apps/mail/app/(routes)/mail/all/` (new) |
| Mail layouts | `apps/mail/app/(routes)/mail/layout.tsx` and all sibling layouts |
| Conversation inbox route | `apps/mail/app/(routes)/mail/conversation-inbox/` (new) |
| Conversation group component | `apps/mail/modules/threads/conversationInbox/ConversationGroup.tsx` (new) |
| Task row component | `apps/mail/modules/threads/conversationInbox/ConversationTaskRow.tsx` (new) |
| Configure panel | `apps/mail/modules/threads/conversationInbox/ConversationInboxFilter.tsx` (new) |
| Conversation inbox tRPC | `apps/server/src/trpc/routes/conversation-inbox.ts` (new) |

---

## Phased Implementation Plan

### Phase 0 — Layout (no DB changes)
1. Add `getNextEvent` tRPC procedure to calendar routes.
2. Build `NextEventWidget` component (static, using `getNextEvent` query).
3. Build `InboxNavWidget` component (reuses existing `useStats()`).
4. Update `SidePanelContainer` default content in mail routes to render the two new widgets stacked.
5. Remove `EmbeddedCedarChat` from mail route sidepanel default.

**Verification**: Right sidebar shows next event + nav list. Chat is gone from mail. "Meeting Prep" opens the linked conversation. Calendar events load from the existing `CalendarDataSync` global sync — no extra tRPC calls needed.

---

### Phase 1 — Internal Label System
1. Add DB migrations: `labels`, `thread_labels`, alter AOP tables for `icon`.
2. Add `labels` tRPC router with all CRUD + thread association procedures.
3. Seed system labels (`priority/important`, `priority/not-important`) as `source = 'cedar'` on user bootstrap / first login.
4. Write one-time backfill: existing `crmConversations.aopId` → `thread_labels`; existing Gmail labels → `labels` rows with `source = 'gmail'`.
5. Modify `syncAopLabel()` behind `INTERNAL_LABELS_ENABLED` flag: write to `thread_labels`, only push to Gmail if `gmail_sync_status = 'pending'`.
6. Update "Deals" folder query to use `listThreadsByLabel`.
7. Expose `icon` field in AOP create/edit UI.

**Verification**: Assigning an AOP no longer creates a Gmail label by default. Threads appear in the Deals folder via `thread_labels`. Backfill accounts for all existing conversations. Gmail label cache still readable via `source = 'gmail'` rows.

---

### Phase 2 — Smart Inbox
1. Add importance classification step to the thread enrichment pipeline (after AOP assignment).
2. Wire classification output to `applyToThread` with `priority/important` or `priority/not-important` label.
3. Seed `inbox/active` and `inbox/done` system labels (Phase 1.1 migration already includes them).
4. Add `markDone(threadId)` and `markActive(threadId)` tRPC helpers (atomic label swap).
5. Create the `/mail/smart-inbox` route with two-section layout, filtered to `inbox/active` threads only.
6. Create the `/mail/done` route backed by `listThreadsByLabel('inbox/done')`.
7. Bind `e` hotkey on thread rows: calls `markDone`, optimistic removal, 5-second undo toast.
8. Add "Smart Inbox", "Done", and "All Mail" entries to left folder nav and `InboxNavWidget`.
9. Add right-click override actions on thread rows for importance labels.
10. Add `user_override` tracking so agent does not clobber user corrections.
11. Add AOP icon badges to thread rows.
12. Create `/mail/all` route (flat unfiltered thread list).

**Verification**: New emails classified within seconds of arrival. Smart Inbox shows correct split. User overrides persist across re-classification runs. AOP icons appear in the nav and thread rows.

---

### Phase 3 — Conversation Inbox

**Goal**: A CRM-style inbox view where threads are grouped by conversation (company/contact), not sorted by recency. Each group surfaces the conversation's next step date, active state, and associated tasks — turning the inbox into a relationship-driven work surface.

#### 3.1 — Route & Layout

**New route**: `apps/mail/app/(routes)/mail/conversation-inbox/`

The view replaces the flat thread list with a grouped layout. Each group represents one `crmConversation`.

**Header controls** (added to the title row, left of the Compose button):
- **Configure** button — opens a panel/dialog to control which conversations appear (filter by AOP, active state, next-step date range, or custom label).

#### 3.2 — Filter Model

The filter operates on two axes simultaneously:

1. **Conversation attributes** — filter conversation groups by: AOP assignment, active/done state, next-step date (overdue / today / this week / no date), and any Cedar label applied to the conversation.
2. **Latest email** — within each conversation, "latest email" signals recency. The group sort order defaults to conversations whose latest email is most recent. The filter can restrict to conversations where the latest email matches a sender, date range, or label.

Filters are persisted in `localStorage` (or a `userPreferences` tRPC call) so they survive page reloads.

#### 3.3 — Conversation Group Row

Each group is a collapsible card. The header row (always visible) contains:

```
┌────────────────────────────────────────────────────────────────────────────┐
│  [Company icon]  Company Name           Next Step: Apr 24   ·  [Active ▾] │
└────────────────────────────────────────────────────────────────────────────┘
```

- **Company icon** — `BimiAvatar`/logo sourced from `crmConversations.company.logoUrl` (same as thread row avatar).
- **Company name** — `crmConversations.company.name` or contact name for 1:1 conversations.
- **Next step date** — `crmConversations.nextStepDate` formatted as a relative or absolute date (e.g. "Apr 24", "Overdue"). Clicking opens an inline date picker to update it.
- **Active badge** (rightmost, pinned) — a pill showing the current active state. Clicking it opens a two-option popover:
  - **Important** — applies `priority/important` label to all threads in this conversation.
  - **All** — removes importance filter; shows all threads regardless of classification.
  - The badge label reflects the current selection ("Important" or "All").

#### 3.4 — Expanded Group Content

When a group is expanded (default for conversations with unread or overdue next step), two sub-lists appear below the header:

**Email list** — threads belonging to this conversation, rendered using the existing `Thread` component (from `apps/mail/modules/threads/threadList/threadItem/components/thread.tsx`). The "latest email" filter from §3.2 determines which threads are shown here. Each thread row is identical to the standard inbox row: avatar, sender, subject, preview, date, hover actions.

**Task list** — tasks associated with this conversation (`crmTasks` rows where `conversationId` matches), rendered in the same visual style as `Thread` rows:
- Left column: task checkbox (marks task complete) replacing the avatar.
- Title column: task title (truncated, same weight/size as thread subject).
- Right column: task due date formatted identically to thread `receivedOn` date.
- Hover actions: complete, delete, snooze (analogous to archive/delete/remind on threads).

```
┌────────────────────────────────────────────────────────────────────────────┐
│  [Company icon]  Acme Corp              Next Step: Apr 24   ·  [Active ▾] │
├────────────────────────────────────────────────────────────────────────────┤
│  [thread row — styled via Thread component]                                │
│  [thread row]                                                              │
├── tasks ───────────────────────────────────────────────────────────────────┤
│  [☐]  Follow up on proposal                                    Apr 25      │
│  [☐]  Send contract draft                                      Apr 28      │
└────────────────────────────────────────────────────────────────────────────┘
```

#### 3.5 — Data Sources

| Data | Source |
|---|---|
| Conversation groups | `crmConversations` (tRPC: `conversations.list` filtered by label/AOP/state) |
| Company logo | `crmConversations.company.logoUrl` → `BimiAvatar` |
| Next step date | `crmConversations.nextStepDate` |
| Active/Important state | `thread_labels` (system labels `inbox/active`, `priority/important`) |
| Thread rows | Existing Zustand thread store, same as all other mail views |
| Tasks | `crmTasks` (tRPC: `tasks.listByConversation(conversationId)`) |

#### 3.6 — tRPC Additions

New procedures (added to existing routers or new `conversationInbox.ts` route):

- `conversations.listForInbox(filters)` — returns conversations with their `company`, `nextStepDate`, latest thread timestamp, unread count, and task count. Accepts filter params: `aopId[]`, `activeState`, `nextStepRange`, `labelIds[]`.
- `conversations.updateNextStep(conversationId, nextStepDate)` — inline date picker update.
- `tasks.listByConversation(conversationId)` — tasks for a conversation, sorted by due date.
- `tasks.complete(taskId)` — marks task done.

#### 3.7 — Configure Panel

The **Configure** button opens a slide-over or dropdown panel:

- **Show conversations**: toggle between "Active only" / "All" / "Done only".
- **Filter by AOP**: multi-select AOP chips.
- **Filter by next step**: radio — All / Overdue / Today / This week / No date set.
- **Default importance filter**: "Important" or "All" (sets the per-group Active badge default for new conversations).
- **Sort by**: Latest email (default) / Next step date (ascending) / Company name (alphabetical).

Settings are persisted per-user.

---

### Phase 3 — Conversation Inbox (Implementation Steps)

1. Add `conversations.listForInbox` tRPC procedure with filter and sort params.
2. Add `tasks.listByConversation` and `tasks.complete` tRPC procedures.
3. Add `conversations.updateNextStep` procedure.
4. Create `/mail/conversation-inbox` route scaffolding (mirrors `[folder]/` layout).
5. Build `ConversationGroup` component: header row with logo, name, next-step date picker, Active badge popover.
6. Build `ConversationTaskRow` component: task-row styled to match the `Thread` component layout (checkbox, title, due date, hover actions).
7. Build `ConversationInboxFilter` (Configure panel): AOP filter, state toggle, sort selector.
8. Wire filter state to `conversations.listForInbox` query params (persisted in `localStorage`).
9. Add "Conversation Inbox" entry to left folder nav and `InboxNavWidget`.
10. Add the **Configure** button to the view's title bar, left of Compose.

**Verification**: Groups render with correct threads and tasks. Next-step date is editable inline. Active badge popover applies importance label to all threads in the group. Configure panel filters and sorts correctly. Filter state survives page reload.

---

## Open Questions

1. **Gmail label read-back**: After Phase 1, do we need to keep reading existing `[Cedar]/aop/*` Gmail labels for legacy threads, or do we rely purely on the backfill?
2. **Org-level labels**: Should `internal_labels` be scoped per-user or per-org? (Current AOP model has both.) For now: per-user, org extension deferred.
3. **Classification model**: LLM-based (Claude call per thread) or heuristic (sender history, header signals)? LLM is more accurate but has cost implications at scale. Recommend: heuristic first pass, LLM for ambiguous cases.
4. **Smart Inbox badge count**: Should the main "Inbox" badge exclude threads in "Not Important", or keep current behaviour? Recommend: keep existing `INBOX` badge; Smart Inbox gets its own count.
5. **Meeting Prep conversation**: Not all calendar events will have a linked `crmConversation`. What is the fallback for the "Meeting Prep" button? Recommend: open a new conversation pre-seeded with event context.