inboxSlice.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 PMpending-channel-draft.tsfeat(tables): sort by a column, and LinkedIn/WhatsApp output cells Two additions to table documents. SORTING. `schema.sort` orders the rows the way the column's TYPE orders — currency numerically, a date by the instant (in the column's own `format`), a select by its DECLARED option order, `_status` by the fan-out lifecycle. One comparator in `table-sort.ts`, shared by the grid and by `read`, so a sorted read and the sorted table cannot disagree. The sort is a VIEW: `tableRows` keeps document order, so `_id` addressing, the ordinal forms and every row's CRDT identity are untouched. Two consequences fall out of that — the row drag handle is withdrawn while sorted, and `read` reports rather than silently adopts a declared sort, because an ordinal resolves against document order on every backend. `table-sort.ts` also absorbs the tolerant cell readers, which existed twice (the Excel writer's and the grid's) and had already drifted on the checkbox vocabulary. LINKEDIN / WHATSAPP OUTPUT CELLS. Both kinds were declared but their payload fell into `GenericCellOutput`, which holds the words and nothing that says who they are for. Real payloads now, addressable by an existing chat OR by a person with no chat yet — which is the lead-list case the column is for. `canSendCellOutput` becomes `sendableCellOutput`, which RESOLVES a cell into a payload whose fields are required, removing three non-null assertions from the send site. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:17 PM