componentsfix(review): archive from any tab, rename an agent in place, and eight bugs a review found Three features that were sitting uncommitted, and the findings from reviewing the whole branch against them. The features. Archive from an open thread was a no-op on every tab whose slug is not literally `inbox` — the Important and Other tabs, a custom inbox, All Mail: `moveThreadsTo` resolved to no labels, logged "No labels to modify" and never made the request. Both the toolbar button and `e` now go through one `useArchiveThread`, which closes the thread on the same frame, puts up an Undo toast, and commits. An agent's title is editable where you are looking at it, through the same `EditableText` a conversation's name uses — displayed as the pretty form, saved as the slug the harness registers it under, and refused when the slug is already taken, because the loser of a duplicate simply stops being reachable from chat. The review's findings, in rough order of what they cost: `chat_messages.citations` was still typed event-only while the frontend had already widened to an event|web union, and four `as Citation[]` casts were the only thing hiding it — a server reader of a persisted web citation got `undefined` for `quote` with no type error. The column now carries the real union and the casts are a zod parse at the boundary. Web citations never rendered while the answer streamed: `toolResultResponseProcessor` read `citations` off the SSE frame and dropped them on the way into the message, so every `[N]` fell through as grey text until the canonical reload swapped it — and never at all if the user cancelled. And the per-turn carry never reset at a turn boundary, so turn five inherited turn one's sources and any later "option [2]" became a live pill pointing at an unrelated page. `openWrittenDocumentIfPanelIdle` promised thread isolation and only checked whether the ACTIVE thread's panel was empty. A background run's document landed in front of whoever was reading a different chat, because `setSelectedArtifact` writes the active thread's slot. It takes the writing thread now, and the test that was named for this case actually exercises it. `mutateThreadContext` is a non-atomic read-modify-write of one JSON column, which was harmless while attach-on-write was gated behind a flag no call site set. It is now unconditional for every chat-turn write, and agents emit parallel tool_use blocks — so two reads saw the same context and the second UPDATE dropped the first one's chip. Now one transaction with the row locked. Dropping the `is_list_field = false` filter from the CRM field read turned it into "every value ever extracted for every deal on the page", on every table read, with eight of them able to reach a cell. Bounded in SQL with `row_number()` per (conversation, field) rather than a global LIMIT, which one talkative deal would have eaten. `readCustomFrontmatterEntries` skipped block-list continuations by their missing colon, but a grant item has one — `- roadmap: rw`, `- Bash(git status:*)` — so a hand-authored agent showed its grants twice, once correctly and once as junk labelled "not acted on", which they are. `getAgentOutputs` built its LIKE prefix from an unescaped `agentId`, and the new `classifyAgentPath` THROWS on an over-matched row rather than misfiling it, so one `_` in an id would 500 the whole Output tab. Escaped, and mapped with a guard. `createBoard` scanned org-wide for a name collision, but board uniqueness is (orgId, userId, path) — so one teammate's "New board" numbered itself off a set of boards they cannot see. `agent.create` wrote `defaultFile` unvalidated while `setDefaultFile` validated it, so a create-time `../other` was silently discarded on read. A run's notification reported the Slack channel it ASKED for rather than the one it reached, which is exactly the customer-visible-vs-private distinction the field exists to draw. Plus: `AgentDocumentView` was imported eagerly by the panel that lazy-loads TipTap to keep it out of the home bundle, and that same edge closed a real import cycle back through `AgentOutputTab → FileBrowser`; the "Default file" radio could never match its own value, so it rendered with nothing selected; two `as` casts where a `find` narrows; a settled mutation that wiped the other textarea's in-progress draft; and `mergeCanonicalPage` threw away the whole scrollback when one boundary row had no timestamp. pnpm --filter @zero/mail: 2949 jest tests, tsc -b --force clean. pnpm --filter @zero/server: 8999 vitest tests, tsc -b --force clean. pnpm deps:check: no violations. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 3, 2026, 10:49 AMhooksfix(review): archive from any tab, rename an agent in place, and eight bugs a review found Three features that were sitting uncommitted, and the findings from reviewing the whole branch against them. The features. Archive from an open thread was a no-op on every tab whose slug is not literally `inbox` — the Important and Other tabs, a custom inbox, All Mail: `moveThreadsTo` resolved to no labels, logged "No labels to modify" and never made the request. Both the toolbar button and `e` now go through one `useArchiveThread`, which closes the thread on the same frame, puts up an Undo toast, and commits. An agent's title is editable where you are looking at it, through the same `EditableText` a conversation's name uses — displayed as the pretty form, saved as the slug the harness registers it under, and refused when the slug is already taken, because the loser of a duplicate simply stops being reachable from chat. The review's findings, in rough order of what they cost: `chat_messages.citations` was still typed event-only while the frontend had already widened to an event|web union, and four `as Citation[]` casts were the only thing hiding it — a server reader of a persisted web citation got `undefined` for `quote` with no type error. The column now carries the real union and the casts are a zod parse at the boundary. Web citations never rendered while the answer streamed: `toolResultResponseProcessor` read `citations` off the SSE frame and dropped them on the way into the message, so every `[N]` fell through as grey text until the canonical reload swapped it — and never at all if the user cancelled. And the per-turn carry never reset at a turn boundary, so turn five inherited turn one's sources and any later "option [2]" became a live pill pointing at an unrelated page. `openWrittenDocumentIfPanelIdle` promised thread isolation and only checked whether the ACTIVE thread's panel was empty. A background run's document landed in front of whoever was reading a different chat, because `setSelectedArtifact` writes the active thread's slot. It takes the writing thread now, and the test that was named for this case actually exercises it. `mutateThreadContext` is a non-atomic read-modify-write of one JSON column, which was harmless while attach-on-write was gated behind a flag no call site set. It is now unconditional for every chat-turn write, and agents emit parallel tool_use blocks — so two reads saw the same context and the second UPDATE dropped the first one's chip. Now one transaction with the row locked. Dropping the `is_list_field = false` filter from the CRM field read turned it into "every value ever extracted for every deal on the page", on every table read, with eight of them able to reach a cell. Bounded in SQL with `row_number()` per (conversation, field) rather than a global LIMIT, which one talkative deal would have eaten. `readCustomFrontmatterEntries` skipped block-list continuations by their missing colon, but a grant item has one — `- roadmap: rw`, `- Bash(git status:*)` — so a hand-authored agent showed its grants twice, once correctly and once as junk labelled "not acted on", which they are. `getAgentOutputs` built its LIKE prefix from an unescaped `agentId`, and the new `classifyAgentPath` THROWS on an over-matched row rather than misfiling it, so one `_` in an id would 500 the whole Output tab. Escaped, and mapped with a guard. `createBoard` scanned org-wide for a name collision, but board uniqueness is (orgId, userId, path) — so one teammate's "New board" numbered itself off a set of boards they cannot see. `agent.create` wrote `defaultFile` unvalidated while `setDefaultFile` validated it, so a create-time `../other` was silently discarded on read. A run's notification reported the Slack channel it ASKED for rather than the one it reached, which is exactly the customer-visible-vs-private distinction the field exists to draw. Plus: `AgentDocumentView` was imported eagerly by the panel that lazy-loads TipTap to keep it out of the home bundle, and that same edge closed a real import cycle back through `AgentOutputTab → FileBrowser`; the "Default file" radio could never match its own value, so it rendered with nothing selected; two `as` casts where a `find` narrows; a settled mutation that wiped the other textarea's in-progress draft; and `mergeCanonicalPage` threw away the whole scrollback when one boundary row had no timestamp. pnpm --filter @zero/mail: 2949 jest tests, tsc -b --force clean. pnpm --filter @zero/server: 8999 vitest tests, tsc -b --force clean. pnpm deps:check: no violations. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 3, 2026, 10:49 AMlibrefactor(mail): stop announcing success, and fix the qualified-date parser Toasts now only fire when something went wrong. 416 success confirmations across 151 files told the user that the thing they had just watched happen had happened — the row was already gone, the draft already sent, the text already in the clipboard. Removing them took the variables and callbacks that existed only to build those messages with them. Thirteen survive, minus the success styling: the ones carrying an Undo button, where the toast is the affordance rather than a notification. Those now use the neutral toast() so they read as "here is your undo", not congratulation. toast.info / toast.warning are untouched — those report neutral or unwanted outcomes, which is not the same thing as celebrating one. Also fixes "mid august" resolving to August 1st in every date picker. chrono has no notion of early/mid/late, so it finds the month, discards the qualifier and answers the 1st — and near a month boundary forwardDate then pushed that past date into NEXT YEAR, so scheduling a send three days out landed eleven months away. resolveQualifiedPeriod runs ahead of chrono and takes the words it cannot express: named months, relative months, weeks and years, each with early / mid / late, rolling forward only when the resolved date has actually passed. And the scheduled email in a thread now renders as an ordinary message rather than a bespoke card: same MailDisplay, a Scheduled badge in the tracking-badge slot, and Edit / Delete replacing reply / reply-all / forward — there is nothing to reply to on mail that has not gone out yet. Edit unschedules before reopening, because the send payload is a frozen snapshot and would otherwise fire the old text alongside the edited one. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 12, 2026, 10:13 PMutilsfix(unibox): a chat row that answers the keyboard, and an R that actually drafts R reported "No emails to select" on a LinkedIn or Slack row, and on an email row it opened the thread and left no composer. Two bugs behind one key. The list handlers resolved their target from a ref that only ever holds an EMAIL id, so a hovered chat row was invisible to every one of them — and anything reached through the bulk selection instead handed `li:<chatId>` to the Gmail driver, which issued modifies for thread ids Gmail has never heard of (x, u, i, h, #). So the handlers now resolve ONE target set and split it: Gmail ids down the optimistic email path, chat rows to the channel path, both halves every time. That deletes actOnSingleInboxId / handleHoveredInboxItem / bulkActOnSelectedInbox along with their dropFromFeed calls for email, which stopped matching anything when the feed moved to the client. r/⇧R open the chat, which is where its composer takes focus. The opener moved to module state because MailListHotkeys is mounted at the root and can never be handed openChannel as a prop; the row click goes through the same function. Spam/trash/archive collapse into Mark done, i stars (a chat row has one flag), h snoozes, and f says it cannot rather than doing something adjacent. The email half: createDraftInThread needs the thread's messages, and a row you press R on has not fetched its body yet — so the first pass fails by design and the whole thing rested on a retry that did not exist. The effect depended on threadId and a callback built from stable store actions, so nothing changed when mail.get landed. It now retries on the messages arriving, attempts once, and clears either way so a stuck action cannot fire in the next thread opened. Also: markChannelRead clears the `u` override as well as marking the source read, or a row marked unread by hand re-bolds on the refetch that call itself schedules. And the seven `as 'linkedin' | 'whatsapp' | 'slack'` casts became a real narrowing. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 2:12 PMindex.tsfix(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 PM