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