appfeat(inbox,two-factor,marketing): channel filter on query tabs, 2FA policy, host routing Three in-flight threads plus supporting docs, committed together. - inbox: the channel badge no longer goes empty on a Gmail-query tab — participatingChannels stops dropping chat channels when the tab rule is mailOnly, so an explicit single-channel badge asks the sources it names. - two-factor: policy module with its own tests, wired through session verification and auth, with the env and runtime-contract keys it needs. - marketing: host routing for the signed-out root, with a redirect test. - docs: inbox channel-filter bug writeup, daily agenda templates, open-tracking v2 notes, and the 2026-08-30 customer feedback audit. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:53 AMcomponentsfix(ui): a menu marks the chosen row with a tick on the right, never a black dot The agent workspace's Folder and Default file submenus put a solid filled disc beside the current value, in a left gutter — shadcn's `RadioItem` default, inherited without anyone choosing it. It out-weighed the label it was marking, it sat in the lane that belongs to the option's own icon, and it looked nothing like the checkbox rows in the same menu, which were already ticking on the right. One mark, one side, for every "which one(s)?" surface: `Check` on the right in a lane every row reserves. Applied to `DropdownMenuRadioItem`, its checkbox sibling, both `ContextMenu` items and the cedar-os copy of the menu; `SelectItem` and `OptionPicker` were already correct. `radio-group.tsx` keeps its dot — there the dot IS the control, not an annotation on one. Written into the design system (crystallized.md → Counts, ticks and ordinals, plus the panel checklist and CLAUDE.md), and guarded by a test, because the dot is a one-line regression and nothing else in the suite would notice. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 1:43 PMconversationsfix: claude reviewJun 5, 2026, 11:21 PMfilesfix(types): close 205 typecheck errors by repairing real client/server drift apps/mail typechecked against a stale apps/server/dist d.ts (the server's package.json points "types" at dist), so a chunk of the reported drift was phantom. Rebuilt it and fixed the four script errors that blocked `tsc -b` in server, which is what regenerates it. The rest was genuine contract drift, fixed at the source rather than cast away — routes that dropped fields their callers still read (agentExecutions, getUpcomingCalendarEvents' conversationId, createCanvas' description), schema splits the UI never followed (conversationUsers vs conversationContacts), and enums that grew server-side but not client-side (TASK_TYPES, ActionStatus, DateFilterOperator). Also deletes code that was already dead: an unreferenced sort popover whose store API is gone, a panel importing a deleted module, the system-skill metadata UI whose mutation was removed deliberately, and a test asserting a store method that no longer exists. apps/server: 4 -> 0. apps/mail: 228 -> 23, all remaining errors being dependency resolution (zod v3/v4, react-router dev/runtime skew, tiptap v2 via novel) rather than code. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 7:45 PMlibfix(mail): a link in a chat thread is a link LinkedIn and WhatsApp send their words as plain text, so a pasted URL rendered as characters. Slack linkified only its own <url|label> tokens — which its composer emits and a bot, an unfurl field or pasted text do not. Bare URLs are now linkified in both, before the mrkdwn formatting pass so the parser cannot read a URL's underscores as italics. The splitter is lifted out of the calendar's LinkifiedEventText into lib/linkify. Links are painted with the action ramp, not primary: primary is near-black on light and near-white on dark, the colour of the words around them. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 18, 2026, 9:38 AMmodulesfeat: the transcript names what it did, and the table it made is reachable A chat that spent twenty calls building a LinkedIn tracker recorded almost none of it: four different people got connection requests under four identical "Executing linkedin-write" lines, the table itself never became a context item, and re-entering the chat reopened the deal instead of the table. Traced end to end; this is the set of causes. The transcript: - Tool labels resolve from the ARGUMENTS, not just the name. Every tool is an action family now, so the name alone collapses a dozen verbs into one row. Rows expand to who / where / whether a message went with it. - A select column's declared option order IS its sort ladder; the tool description now says so, and the transcript shows the ladder. LinkedIn caps: - A send's provider-id lookup gets its own budget (connect/dm/inmail_lookup), mirroring the send it serves. Charged to profile_view, a day of browsing refused invitations that had 80/week of connect headroom left. - Connection requests go out with NO note unless the user asked for one — enforced at execute time, not just described. Getting back to what the agent made: - The table tool files a context chip on create/import, as write-document has always done; delete pulls it back off. - A thread remembers what it was displaying (chat_threads.context), so re-entry restores the table rather than re-deriving the deal. - A fan-out opens a conversation-scoped table inside its deal's Files tab. - 'table' is a valid write type on the document family — the tool was telling the agent to pass a value its own schema rejected, which is what drove a delete-and-rebuild that orphaned the open document. The grid: - Title, then description, then the commands — the order the questions arrive in; a URL or address in a cell is a real link; a deleted table says so instead of rendering on from the local Y.Doc cache. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:43 PMtimezonefix(crm): "3h ago", not "Today", for everyone west of UTC `formatRelativeDate` read two different calendars. The day branch built its comparison from UTC components — deliberately, so a date-only value stored at midnight UTC does not slide back a day for US viewers — and said so in a comment ending "BOTH sides must read the same clock". The sub-24h branch three lines above it asked date-fns `isToday`, which reads the LOCAL calendar. The one branch nobody re-checked was the one the comment was about. West of UTC the two disagree for most of the evening. At 04:37Z on the 31st — 00:37 in New York — a touch from 3h41m earlier is 20:55 on the 30th locally but still the 31st in UTC. So `isToday` was false, the hour branch was skipped, and the day math, which had both on the 31st, answered "Today". A rep opening a deal after 8pm saw a meeting from that afternoon labelled as though the hour did not matter. The UTC day values are now computed once, above both branches, and the hour branch tests those instead of the ambient zone. `isToday` is gone with it. ── Why the suite could not catch this ── Every CI runner is UTC, and in UTC the two calendars cannot disagree, so all five existing tests passed on the broken code. The suite even documented the hole — "keys off the viewer's LOCAL calendar day by design, so it is not timezone-stable" — right under a test asserting "3h ago", which is exactly the assertion that is not stable. UTC is not a neutral default for date code; it is the blind spot. So there is now a second pass, west of UTC: `tests/timezone/`, run by `pnpm --filter @zero/mail test:tz` and by its own CI step, excluded from the main run so it cannot pass vacuously there. TZ has to be set before the process starts — V8 caches the zone on first use, so neither assigning `process.env.TZ` in a test nor a custom testEnvironment can move it (I tried both; a guard test in the file now fails if the zone ever stops applying). Verified the way a regression test has to be: the new suite fails on the old implementation with exactly "Expected 3h ago, Received Today", and passes on the fix. Co-Authored-By: Claude Opus 5 <<email>>Sep 8, 2026, 1:51 PM