componentsfix(mail): ship the modules 51bc228ab left behind, and stop mailing malformed MIME 51bc228ab committed the google.ts send-path changes but not the two modules they import, so HEAD does not compile. This adds them, plus the rest of the fix they were part of. Cedar builds its own RFC822 and hands it to gmail.messages.send, so every shaping decision Gmail's composer would have made is ours. Four were wrong, and together they were enough for Microsoft EOP to silently quarantine a legitimate reply — no bounce, a SENT label, and "Email created and sent successfully" in our own logs. Sam MacGregor's 2026-09-10 reply to Theresa Keown never arrived; she wrote back two days later asking if she had the wrong address for him. She did not. - text/plain alternative derived from the HTML, on the send AND draft paths - non-X- custom headers refused at the driver boundary; the composer's bare Thread-Id becomes X-Cedar-Thread-Id - RFC 2047 encoded-words undone where the payload is plain ASCII (mimetext base64-wraps every Subject and display name with no way to opt out) - quoted inline images resolved from /api/inline-image reference urls to real cid: attachments, so a root-relative url and a connection uuid stop going on the wire - X-Mailer: Cedar, so an audit can attribute a message instead of fingerprinting it The fifth signal, duplicate Received headers, survives a fully corrected message and is stamped by Gmail on API injection — detected and reported, but not ours. audit-outbound-mime.ts walks real SENT mail through the same detector the tests and the live verification assert on. Across 59 accounts / 3158 messages, Thread-Id is the only non-standard header present anywhere: 52 messages, 10 accounts. headless-send-verify gains assertions F-I against messages.get format=raw, and its cleanup now trashes by thread id via modifyLabels. It previously called users.messages.delete, which needs the full mail.google.com scope Cedar does not request — the old comment blaming a missing gmail.modify scope was wrong. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 13, 2026, 1:12 PM3 defectshooksfix: close the six audit findings on the send/telemetry work A — the relay trusted the client to redact. It now enforces its own denylist on normalized keys and drops email-shaped strings, so a mistake in one browser build cannot put a recipient or a subject into Axiom. B — teardown and keepalive were one flag. A send carrying attachments is too large for keepalive, so it fell through to awaiting the session gate while the page was going away, which is exactly how the request got lost. They are now separate: every teardown commit skips the gate, keepalive stays size-limited. C — the volume caps could silence the one event an incident needs. The sampler already refused to drop a send or a failure, but the session cap and the buffer eviction knew nothing about that, so a long-lived tab went quiet for everything after 2,000 events and overflow evicted oldest-first regardless. Sends and failures are now exempt from the session cap and evicted last. sanitizeEvent carries the flag onto the buffered copy — without that the eviction loop read undefined on every entry and the exemption did nothing, which the new test is what caught. D — findOrCreateEmailEvent (singular) never wrote rfc_message_id, so events created through that path stayed invisible to the RFC dedupe added in 51bc228ab. E — the client request id was a timestamp plus a counter that resets on page load. Two tabs, or a reload inside the same millisecond, minted identical ids and the join then matched unrelated requests. It is a uuid now. F — residue from the 22-row phantom cleanup. Two analytics_draft_actions rows still pointed at messages Gmail 404s; the table has no FK, so nothing cascaded and nothing dangled visibly, but attribution-gap.ts matches drafts to sends by message_id and the fallback only runs when that column is NULL. Both real Cedar-drafted sends were being reported as unattributed. Repointed to the surviving message, verified against live Gmail, before-state kept. The one agent_executions row with a NULL event_id is deliberately left alone: the run was triggered by the phantom, and repointing it at a different message would invent causation. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 6:39 AMstorefeat(inbox): unibox keyboard parity, undo-send that puts the draft back, sharper chat context Unibox now navigates like the email list — j/k, Enter, Escape all walk whichever list is on screen via a new `activeListSource` on the thread slice, and Escape clears the full selection (focused row, bulk checkboxes, ambient conversation) instead of only the keyboard cursor. Undo send stops round-tripping through localStorage and a URL rewrite: the send records where the draft came from and undo reopens that surface, restoring the un-quoted body onto the draft row (recreating it when the send removed it). Chat context: an auto-committed primary conversation is provisional while the chat is empty and drops when you move on, the context cards follow the deal you're actually on, threads with no conversation get a link prompt, and the conversation surface shows a keyboard legend rather than restating cards already on screen. Also: agenda Trash removes the row outright, Next steps renders as one card with its booked calls beneath, and channel chats focus the composer on open.Jul 30, 2026, 11:54 PMutilsfix(drafts): four different failures shared one sentence Every draft-save failure in prod said "Failed to save draft". Among them was invalid_grant — a dead Google token, where retrying is the one thing that cannot work. The message sent the user back to the keyboard to do it anyway. Classify the cause and say it. A dead token names itself and carries a Reconnect action; a provider fault says the text is safe rather than implying the user should retype; a transport failure says so. The toast also carries a short trace ref, so "it said X" becomes a lookup in Axiom instead of a search across four identical strings. (design: bug-draft-saving-duplicates-2026-09-01 phase 4) Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:45 PMBUGREPORT-draft-send-failed-toast.mddocs: add investigation reports and bug writeups Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jun 21, 2026, 7:43 PMBUGREPORT-duplicate-draft-on-type.mddocs: add investigation reports and bug writeups Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jun 21, 2026, 7:43 PMBUGREPORT-optimistic-timeline-desync.mdfix(mail): stop losing deferred sends, and stop recording unsent mail as sent Traced from a report that an email sent in Cedar never reached Gmail while the deal timeline showed it as sent. Four defects, each independently reproduced. Undo-send dropped failures. Of the three paths that commit a held-back send, only the 5s timer reported one; force-send and page teardown discarded the promise with `.catch(() => {})`, leaving the optimistic writes standing over an email that never went out. All three now dispatch through one commitPendingSend. The teardown guard was unreachable. It lived in the composer, which unmounts the instant a send is deferred, so for almost the whole undo window nothing watched for the tab closing. Moved to module scope beside pendingSend, and extended to every pending send rather than only ones carrying attachments. Keepalive never applied to a batched send. The guard matched the whole pathname against '/mail.send', but httpBatchLink puts a comma-joined procedure list there. Ingestion recorded unsent mail as sent. Gmail's pub/sub sync fires ~1s after a change, fast enough to catch a message mid-send: not a draft, so the only state check let it through, and direction was inferred from the sender address alone. Dedupe keyed on Gmail's internal messageId — the one id that differs between the transient and committed copies — so the same email was written twice. Now gated on Gmail's SENT label and deduped on the RFC Message-ID, whose column and partial index already existed for this purpose. Threading depended on which surface composed the reply. The composer only built In-Reply-To/References when it held a replyToMessage, so a reply written from the conversation surface shipped with none and Gmail opened a new conversation. parseOutgoing now derives the chain from the threadId via the existing deriveThreadingHeaders, covering both send paths regardless of caller. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 12, 2026, 11:36 AMDESIGN-duplicate-draft-reconciliation.mdwip: snapshot in-flight work across agenda, brain, conversations and chat Pre-existing uncommitted work from concurrent sessions, snapshotted so the agenda Current|Future restructure lands as one commit per design-doc phase. Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 21, 2026, 2:29 PMDESIGN-userEdited-drafts.mdtest: race coverage + retire old design doc (design: userEdited-drafts phase 4) thread-data-sync-race.test.tsx now exercises the userEdited rule: locally- edited drafts survive interleaved foreground + prefetch mail.get arrivals. The old generation-tracker-based "stale save callback" assertion is replaced with a pure store-level interleaving test, since the tracker no longer exists. Old design doc at docs/design/draft-ownership-and-mail-get-reconciliation.md is superseded by apps/mail/modules/drafting/DESIGN-userEdited-drafts.md and deleted. Co-Authored-By: Claude Opus 4.7 (1M context) <<email>>May 20, 2026, 12:38 AMDRAFT_SESSION_ARCHITECTURE.mdfeat: [threads] new stable draft sessionid systemJan 11, 2026, 5:09 PMdrafting.mdfeat: [threads] drafts now use sessionDraftIdJan 11, 2026, 4:58 PMindex.tsbuild(deps): resolve the three dependency skews behind the last typecheck errors zod: the root declared zod 4.1.1 purely to satisfy zod-to-json-schema, which hoisted v4 into node_modules/zod. @hookform/resolvers has no zod dependency of its own, so its .d.ts resolved that v4 while apps/mail builds schemas with catalog v3 — every zodResolver call was a type error. Root now tracks the catalog, and a packageExtension pins resolvers to v3 explicitly so this can't drift back in on a hoist order change. react-router: @react-router/dev was a minor behind the runtime and emitted GetAnnotations<Info, false> into .react-router/types, against a one-parameter GetAnnotations. Bumped the generator to match. tiptap: `novel` pins TipTap v2, so defaultExtensions was feeding v2 extension classes into a v3 editor — a type error over a real runtime hazard. Every consumer of novel's React surface (EditorBubble, its AI bubble menu, the slash command items) was already dead code, so this drops to a straight swap of the extension classes for their v3 equivalents, and novel and its patch go with it. AIHighlight, CharacterCount and CustomKeymap are dropped — novel-only, and unreferenced outside extensions.ts. Underline now comes from StarterKit, which bundles it in v3. Also declares @testing-library/user-event, which email-composer-send.test.tsx imports but nothing ever depended on, and fixes that suite's wrong relative mock paths. It still doesn't run — its jest.mock factories close over out-of-scope variables — but that is a test rewrite, not a type fix. apps/mail: 23 -> 0. `pnpm types` is green across the workspace. Test results are unchanged (155/165 suites, same 23 pre-existing failures) and `react-router build` succeeds. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 8:10 PM