format-inbox-count.tsfeat: stacked inbox view (design: stacked-inbox-design phase 4) When inboxLayout === 'stacked' and the user is on /mail/inbox, replace the per-inbox tab strip with a single synthetic [Agenda][Inbox] header and render every configured inbox (Important, customs..., Other) as a vertically stacked, collapsible section. Each section runs its own listThreads infinite query via useInboxThreads(inbox, { pageSize: 25 }) and exposes its own prev/next paginator via usePageWindow — paging one section doesn't disturb the others. SYSTEM_INBOXES_BY_LAYOUT.stacked now mirrors important_other so both system inboxes live in `inboxes` for the section iterator; routing for the synthetic Inbox tab uses the existing /mail/inbox standard slug. formatInboxCount moved to lib/format-inbox-count.ts so SplitInboxTabs and InboxSection share one formatter. No user-facing toggle yet — that lands in phase 5. Co-Authored-By: Claude Opus 4.7 (1M context) <<email>>May 24, 2026, 9:32 PMinbox-compiled-query.tsrefactor: unify inbox storage and retire split-definitions Co-Authored-By: Codex Sonnet 4.6 (1M context) <<email>> Co-authored-by: Cursor <<email>>Jun 1, 2026, 11:51 PMinbox-query-params.tsfeat(mail): CRM-filtered inboxes — AOP + conversation-field rules (design: pipeline-inbox-crm-stage-filter phases 1-4) An inbox can now carry a `conversationFilter`: restrict `label:INBOX` to threads whose linked CRM conversation matches an AOP + field rule. "In pipeline" becomes a property of the deal rather than something an AI label re-derives per email. The rule is structured, not a query token. It rides beside `compiledQuery` and resolves through buildConversationFilterConditions — the SAME builder the /conversations surface uses — so the two can never disagree about what a filter means. The filter schema itself moved to lib/conversation-filter-schema.ts and is now shared with crm.listConversations. Routing safety is the load-bearing part: Gmail cannot answer these queries, but the query text is an ordinary `label:INBOX`, so nothing downstream could tell. `mirrorOnly` (derived from the filter's presence, not the parsed query) closes all four places that would otherwise hand it to Gmail and silently return the user's entire inbox — a wrong answer that looks right. Two guards stop such an inbox from partitioning Inbox: alsoShowInImportant defaults on, and buildImportantQueryExclusions unconditionally skips filtered inboxes — without which the default inbox compiles to `label:INBOX -(label:INBOX)` and renders empty. Headless: `cedar-cli mail create-inbox --conversation-filter`, `inbox-count`, and `load-inbox` (now prints linked conversation + stage). Verified end to end as <email>: 148 threads, an exact thread-id set match against raw SQL, zero overlap with the inverse filter. Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:04 PMroute-inbox.tsfeat(inbox): retire the server-merged feed, and make one email read serve both lists (design: inbox-triage phases 8 + 0) Finishes Phase 8 and lands Phase 0. RETIRED — `inbox.listItems` and `assembleInboxFeed`, with `fetchEmailItems`, `inboxCompiledQuery`, the folder→query fallbacks and `resolveEmailLabelTerms`. feed.ts drops 875→500 lines and no longer reads mail at all. Its 4 test files are RETARGETED, not deleted: scope onto `participatingChannels` and the three sources' SQL, and the real-DB files onto the live `inbox.listChannelItems` route. TWO PAGING BUGS the server-merged feed was hiding, both found by retargeting those tests onto a single source — three others had been covering for each one: * `mergeAndPage` truncated a single-source feed at page two: the sources resume at `<= cursorSortedAt`, so every page re-read its boundary row and spent the `limit + 1` that proves "more remains". It now takes a REQUIRED `sourcesSaturated` — required so a new caller cannot silently reintroduce it. * The sources ordered by `last_message_at` alone while the pager tiebreaks by id, so a LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted ABOVE the minted cursor — gone for good. All six source queries now order `(last_message_at DESC, <itemId> COLLATE "C" DESC)`; the collation is load-bearing, since the default one folds case and ignores punctuation and would disagree with `compareItemsDesc`. Measured: three rows vanished from a paged walk of a real feed. PHASE 0 — one email read, one route resolver: * The `headChanged` latch is per-surface. Phase 8 gave `mail.listThreads` a second caller, so a single per-connection flag meant whichever surface read first consumed the signal. `markPendingHeadChange` fans out to every surface, because the reconcile dedup key carries none and the second reader routinely joins the first's in-flight run. * `use-route-inbox.ts` resolves the active inbox from the URL for BOTH lists; the persisted `settings.activeInboxId` fallback is gone. Closes the important/other and system-folder divergences as well. * `drafts.create` awaits `surfaceDraftInInbox`, and the label join row is born with its ordering key instead of being backfilled by a second statement. compose-feed becomes self-verifying now that its diff target is gone: ordered, unique, stable-across-paging, and watermark-respecting, checked over real paging rounds and each probed against a deliberately broken history. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:56 PMsplit-query-normalization.tsfix: stabilize split inbox routing and reminder state Normalize split-inbox query behavior across client/server and apply optimistic reminder updates so thread state stays consistent while async reminder workflows complete. Co-authored-by: Cursor <<email>>May 31, 2026, 8:22 PMsystem-folder-splits.tsfix: add conversation_id to structured logs for agenda access issues When loading or saving conversation documents (agendas), permission errors now include the conversation_id, user_id, org_id, and document path to help debug access denied errors like the one Alessandro encountered. - [getDoc] Add conversation_id to both by-id and by-lookup permission logs - [updateHtmlContent] Add conversation_id to document save permission logs - [applyUpdate] Extract and log conversation_ids from agenda task changes This makes it much easier to diagnose permission issues when users get "Conversation not found or access denied" errors on agenda documents. Co-Authored-By: Claude Haiku 4.5 <<email>>May 26, 2026, 11:26 AMunread-filter.tsfeat(inbox): retire the server-merged feed, and make one email read serve both lists (design: inbox-triage phases 8 + 0) Finishes Phase 8 and lands Phase 0. RETIRED — `inbox.listItems` and `assembleInboxFeed`, with `fetchEmailItems`, `inboxCompiledQuery`, the folder→query fallbacks and `resolveEmailLabelTerms`. feed.ts drops 875→500 lines and no longer reads mail at all. Its 4 test files are RETARGETED, not deleted: scope onto `participatingChannels` and the three sources' SQL, and the real-DB files onto the live `inbox.listChannelItems` route. TWO PAGING BUGS the server-merged feed was hiding, both found by retargeting those tests onto a single source — three others had been covering for each one: * `mergeAndPage` truncated a single-source feed at page two: the sources resume at `<= cursorSortedAt`, so every page re-read its boundary row and spent the `limit + 1` that proves "more remains". It now takes a REQUIRED `sourcesSaturated` — required so a new caller cannot silently reintroduce it. * The sources ordered by `last_message_at` alone while the pager tiebreaks by id, so a LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted ABOVE the minted cursor — gone for good. All six source queries now order `(last_message_at DESC, <itemId> COLLATE "C" DESC)`; the collation is load-bearing, since the default one folds case and ignores punctuation and would disagree with `compareItemsDesc`. Measured: three rows vanished from a paged walk of a real feed. PHASE 0 — one email read, one route resolver: * The `headChanged` latch is per-surface. Phase 8 gave `mail.listThreads` a second caller, so a single per-connection flag meant whichever surface read first consumed the signal. `markPendingHeadChange` fans out to every surface, because the reconcile dedup key carries none and the second reader routinely joins the first's in-flight run. * `use-route-inbox.ts` resolves the active inbox from the URL for BOTH lists; the persisted `settings.activeInboxId` fallback is gone. Closes the important/other and system-folder divergences as well. * `drafts.create` awaits `surfaceDraftInInbox`, and the label join row is born with its ordering key instead of being backfilled by a second statement. compose-feed becomes self-verifying now that its diff target is gone: ordered, unique, stable-across-paging, and watermark-respecting, checked over real paging rounds and each probed against a deliberately broken history. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:56 PM