CedarCopilot wants to merge 3 commits into staging from feat/inbox-client-composed-feed
Live on prod, 1 of 11 behaviors degraded
Production telemetry shows 288 concrete inbox.listChannelItems executions since 2026-09-01T07:01:02Z: 284 successes and 4 tied errors (1.4%). Successful calls averaged 1056 ms; failed calls averaged 1692.5 ms. All failures were `Unexpected end of JSON input`, so the route is.
Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.
No positive production runtime evidence was found since 2026-09-01T07:01:02Z (~71h): 0 OTEL spans for GET /login or any login-named route/span, 0 spans containing required_scopes_missing, 0 server spans for deployed commit 7e84a6d739bd7a6f9af82297725fa8527539dbb3, and 0.
Verification reached its budget before a verdict.
Libra has verdicts on 1 of 11 tracked behaviors on prod; 10 are still being checked. Libra checks hourly for 3 days after each deploy.
Implements Phase 8 and Phase 0 of apps/mail/docs/inbox-triage.md.
The unibox is now assembled in the browser from one query per channel instead of one server-merged page. inbox.listItems and assembleInboxFeed are deleted; feed.ts drops 875 → 500 lines and no longer reads mail at all.
Email is read through mail.listThreads , the router, with its reconcile, drafts union and Gmail fallback. Previously the unibox read the mirror directly through listThreadsFromDb, which is how the two lists came to disagree about which threads exist. Measured on a live inbox: a 20-row inbox.listItems page carried 5 email rows and omitted 9 threads newer than chat rows it did include.
Three rules that used to be copied are now shared, each in one place:
| rule | lives in | callers |
|---|---|---|
| the cross-channel merge + watermark | feed-merge.ts (browser-safe, @zero/server/inbox/merge) | the client store, the headless driver |
| which channels participate | feed-scope.ts | inbox.listChannelItems, inbox.getFeedScope |
| which inbox a route is on | route-inbox.ts | useThreads, the unified feed |
conversationFilter never crosses the wire , inbox.getFeedScope has a .strict() output schema that cannot carry it, and each route re-resolves the rule from inboxId itself.
mergeAndPage truncated a single-source feed at page two. Sources resume at <= cursorSortedAt, so every page re-read its boundary row and spent the limit + 1 that proves "more remains". Now takes a required sourcesSaturated , required so a new caller cannot silently reintroduce it.LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted above the minted cursor , excluded forever. All six source queries now order (last_message_at DESC, <itemId> COLLATE "C" DESC). The collation is load-bearing: the default one folds case and ignores punctuation, and would disagree with compareItemsDesc. Three rows vanished from a paged walk of a real feed before this.headChanged latch was per-connection, so once the unibox became a second caller of mail.listThreads whichever surface read first consumed the signal. Now keyed per surface, and a divergence fans out to every surface , the reconcile dedup key carries none, so the second reader routinely joins the first's in-flight run.settings.activeInboxId fallback is gone; it let the two lists ask different questions about the same route.drafts.create awaits surfaceDraftInInbox, and the label join row is born with its ordering key rather than backfilled by a second statement.Headless, as the operator, against real data:
CEDAR_API_URL=http://localhost:8790 pnpm --filter @zero/server exec tsx \
src/mail-admin/cli.ts compose-feed --for <email> [--inbox "<name>"] [--rounds 3]
compose-feed runs the browser's own composition , same routes, same mergeChannelFeeds , and checks four invariants over real paging rounds: ordered, unique, stable (each rendered list is a literal prefix of the next), watermark. Each was probed against a deliberately broken history. Green on the plain unibox, a mail-only split, and a CRM-filtered inbox.
tsc -b --force clean in both packagesAdds /onboarding-setup , a sidebar-driven flow over the four surfaces a new user actually has to configure (Inbox, sub-inboxes, Tasks, Pipeline), each step paired with a **live pre
Libra has not measured any production surfaces for this change yet.