use-activate-column-sort.tsfeat(crm): split filters and sorting into separate popovers, sort from a header wins Filtering and sorting were one popover with one horizontal row of badges, where badge order silently encoded sort priority. Order now only means something in the sort list, which is the only list you can drag. Sorting a column from its header takes priority 0 and pushes the rest down, rather than landing last where it has no visible effect. Shared by all seven column popovers plus toggleColumnSort, so every entry point behaves the same. Column popovers opened from a list are scoped to that list's concern and anchor beside it like a nested menu; a "Set filter" / "Set sort" button hands the column to the other list. Enum fields keep one list for both — drag is the sort order, click is the filter — so their affordances split by mode instead. Drops ~750 lines of dead code (FilterSortBadgeRow, the deprecated inline row, OwnerUserFilter). The drag-reorder test now runs against the real function instead of a hand-copied duplicate. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 4:29 PMuse-aop-filter-columns.tsfeat(mail): build an inbox's CRM rule with the real conversation filter UI (design: pipeline-inbox-crm-stage-filter phase 5) Adds a "Linked deals" section to the inbox editor: pick AOP(s), then filter on that AOP's own stages and custom fields. The column set is derived from the selection, so Customer Success offers its stages and Deals offers discovery → closed won, with no hard-coded enum anywhere. This reuses FilterSortPopoverContent rather than growing a second filter UI. The new `{ kind: 'inbox' }` scope is draft-local — the inbox does not exist yet while the form is open — so it carries its own config + setter instead of a store slice. Sharp edge found while wiring it: FilterSortPopoverContent only uses the adapter for add/remove/clear. The six leaf column popovers resolve their write target from a `canvasId` prop, falling back to the GLOBAL CRM slice when absent — so every click in the inbox builder would have silently re-filtered the user's /crm page and never touched the draft. They now consult a ColumnFilterOverride context first; a test asserts the value lands on the draft AND that the global slice stays untouched. The pre-existing 'sidebar' scope has the same latent bug (noted, not changed here). Also seeds docs/wiki entries for the mail read path and this subsystem, and comments the deleted_at sharp edge at all three SQL builders. Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:28 PMuse-aop-refresh-watcher.tsrefactor: collapse the duplication the channel-attach and AOP-async work introduced Follow-up to a structural review of this branch. Behaviour-preserving throughout; what changes is how many places have to agree with each other. - Change detection had two implementations. The Gmail freshness probe re-derived the stale-replica/unchanged verdict that the full-fetch path computes 60 lines below, and the divergence mode is silent: the probe seals a thread the authoritative path would have re-read. Both now consume one `unchangedVerdict()`. - `messageSetChanged` reads only message ids and draft state, so it now takes that shape instead of a whole `IGetThreadResponse`. The probe passes its result straight in, and the `as IGetThreadResponse` that fabricated a thread from the S3 snapshot is gone — a fourth field read there is now a compile error, not a stale comparison. - The conversation-ownership authz guard existed three times (Gmail, Slack, LinkedIn), with its own connection lifecycle each, and had already drifted on error handling. Now `services/crm/conversation-access.ts`, following `thread-access.ts`'s shape. - The family→granular dispatch hop was written out longhand in eight branches, next to a ninth copy of `isValidationError`. Now `family-tool-dispatch.ts`. mailTool's comment warning that a shared unwrapper degrades the payload type to `{}` was a real hazard, so the generic is pinned by an `expectTypeOf` test rather than an assertion. - LinkedIn's chat/message output schemas were `z.object({}).passthrough()` — `{}` in JSON Schema, i.e. an agent-facing contract that describes nothing. Declared once in the family schemas and shared with the granular tools. Typing them surfaced that `lastMessageAt` is a Date, not a string. - The frontend's terminal-execution-status list was a hand-copy of the column's union; a renamed status was a spinner that never stops rather than a type error. Both now derive from `db/execution-status.ts`. - `dispatchAopChangeRefresh` documented "never rejects" but acquired the DB handle outside the try and released it in an unguarded finally. Both are now inside the guards, plus the terminal `.catch` every other detached tRPC call already has. - messageRenderers.tsx 1988 -> 1433: the tool-display table is data, not rendering. Added `withGmailSpan` for the new Gmail probe rather than adding a 28th hand-rolled span block to a 5k-line file. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 10, 2026, 1:46 PMuse-bulk-selection.tsfix: shift+s for conversationItemsJan 31, 2026, 10:39 AMuse-canvas-aggregates.tsrefactor(crm): use absolute imports for the new canvas modules CLAUDE.md requires absolute imports; the canvas-metrics work introduced six relative ones, which is what Greptile flagged. Only the specifiers this PR added are rewritten — pre-existing relative imports in these files are left alone so the diff stays about the finding. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 15, 2026, 5:32 PMuse-canvas-configuration.tsfeat(mail): build an inbox's CRM rule with the real conversation filter UI (design: pipeline-inbox-crm-stage-filter phase 5) Adds a "Linked deals" section to the inbox editor: pick AOP(s), then filter on that AOP's own stages and custom fields. The column set is derived from the selection, so Customer Success offers its stages and Deals offers discovery → closed won, with no hard-coded enum anywhere. This reuses FilterSortPopoverContent rather than growing a second filter UI. The new `{ kind: 'inbox' }` scope is draft-local — the inbox does not exist yet while the form is open — so it carries its own config + setter instead of a store slice. Sharp edge found while wiring it: FilterSortPopoverContent only uses the adapter for add/remove/clear. The six leaf column popovers resolve their write target from a `canvasId` prop, falling back to the GLOBAL CRM slice when absent — so every click in the inbox builder would have silently re-filtered the user's /crm page and never touched the draft. They now consult a ColumnFilterOverride context first; a test asserts the value lands on the draft AND that the global slice stays untouched. The pre-existing 'sidebar' scope has the same latent bug (noted, not changed here). Also seeds docs/wiki entries for the mail read path and this subsystem, and comments the deleted_at sharp edge at all three SQL builders. Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:28 PM1 defectuse-canvas-conversations.tsperf(crm): give conversation rows their own event projection A row draws a dot timeline and three "last <type> at" columns, and nothing else — but crm.listConversations was building up to seven nested sub-objects per event through eight LEFT JOINs to feed it. Rows now get five scalars and a flat threadId, ~3x smaller per event, so the cap can move 15 -> 60 and draw three times the history for no more bytes: measured on a 50-row page, 547KB over 641 events before, 538KB over 1,919 now. That cap was also quietly wrong. "Last meeting" was derived by scanning whatever events the list happened to ship, so a deal whose last meeting fell outside its 15 most recent rendered blank — 53 of the 304 conversations with a meeting, in the org this was measured on — while SORTING by the same field used an unbounded MAX(occurred_at) and placed it somewhere the column contradicted. The three columns now read scalars computed over the whole history, from the same expressions the ORDER BY builder already used. Splitting the projections needed guards the single shared type had been hiding: - setConversations takes an explicit `projection`. Both queries write one store key and replace wholesale, so a list refetch would otherwise empty the timeline of whatever deal was open — the same bug that previously just shrank it to 15 (client-data-architecture.md §1.2). - getEventType/getEventTitle read `eventType` rather than sniffing which sub-object came back from a join, which equated "this join returned nothing" with "this is a note" and would have greyed out every dot. - a dot click opens EventDetailDialog on an id and fetches the full event, since the row no longer carries one. Also carries the in-flight pending-conversation-field-writes mask and its tests: it edits the same setConversations body, so it cannot be split out. Co-Authored-By: Claude Opus 5 <<email>>Sep 2, 2026, 11:01 PM1 defectuse-canvas-list-variables.tsrefactor(crm): use absolute imports for the new canvas modules CLAUDE.md requires absolute imports; the canvas-metrics work introduced six relative ones, which is what Greptile flagged. Only the specifiers this PR added are rewritten — pre-existing relative imports in these files are left alone so the diff stays about the finding. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 15, 2026, 5:32 PMuse-crm-canvas-refresh.tsperf(crm-canvas): remove per-deal CRM sync from canvas refresh The canvas refresh was batching all visible conversation IDs and calling refreshDealsFromProvider for each batch, plus discoverNewDealsFromProvider. This is unnecessary now that the nightly background sync runs automatically. The canvas refresh now only invalidates the TanStack Query cache and calls onRefetch, which is sufficient for picking up data already written by the background sync. The individual opportunity view (use-refresh-conversation) retains its explicit CRM pull since that is an on-demand user action. Co-authored-by: Cursor <<email>>May 31, 2026, 10:27 AMuse-crm-configuration.tsfix(mail): hide background (sub-event taxonomy) fields from conversation field UI Background org fields (`background === true`) are internal sub-event taxonomy "moments" (discovery questions, objections, MEDDPICC signals) meant only for analysis. They were leaking into every user-facing field surface — CRM/canvas/ sidebar columns, group-by and CRM field-mapping pickers, the conversation overview grid, the strategic overview, and the working-memory card — because no frontend path filtered them out. Centralize the fix in a single shared helper, `getVisibleFieldDefinitions` (mirroring the server's `pickBackgroundFields`), and route every display consumer of `aop.customFieldDefinitions` through it. The taxonomy editor (IntelligenceFieldsEditor) still reads background fields directly, and admin/field editors are intentionally left untouched. Also declares `background?: boolean` on the frontend `CustomFieldDefinition` type, which previously omitted the flag even though it exists at runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Aug 2, 2026, 1:40 PM1 defectuse-crm-conversations.tsperf(crm): give conversation rows their own event projection A row draws a dot timeline and three "last <type> at" columns, and nothing else — but crm.listConversations was building up to seven nested sub-objects per event through eight LEFT JOINs to feed it. Rows now get five scalars and a flat threadId, ~3x smaller per event, so the cap can move 15 -> 60 and draw three times the history for no more bytes: measured on a 50-row page, 547KB over 641 events before, 538KB over 1,919 now. That cap was also quietly wrong. "Last meeting" was derived by scanning whatever events the list happened to ship, so a deal whose last meeting fell outside its 15 most recent rendered blank — 53 of the 304 conversations with a meeting, in the org this was measured on — while SORTING by the same field used an unbounded MAX(occurred_at) and placed it somewhere the column contradicted. The three columns now read scalars computed over the whole history, from the same expressions the ORDER BY builder already used. Splitting the projections needed guards the single shared type had been hiding: - setConversations takes an explicit `projection`. Both queries write one store key and replace wholesale, so a list refetch would otherwise empty the timeline of whatever deal was open — the same bug that previously just shrank it to 15 (client-data-architecture.md §1.2). - getEventType/getEventTitle read `eventType` rather than sniffing which sub-object came back from a join, which equated "this join returned nothing" with "this is a note" and would have greyed out every dot. - a dot click opens EventDetailDialog on an id and fetches the full event, since the row no longer carries one. Also carries the in-flight pending-conversation-field-writes mask and its tests: it edits the same setConversations body, so it cannot be split out. Co-Authored-By: Claude Opus 5 <<email>>Sep 2, 2026, 11:01 PMuse-crm-presets.tsrefactor(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 PMuse-crm.tsfeat(agents): all backend phases 4-14 (design: agent-workspace) Phase 4 playbook source editing — byte-offset splice, never a reserialize Phase 5 per-agent file attribution (document_updates.actor_agent_id/run_id) Phase 6 MCP tool policy captured at connection setup + call-site enforcement Phase 7 per-agent capability grants, intersected with the connection ceiling Phase 8 chat ACTOR binding + SUBJECT expression index + merge repoint Phase 9 agent memory: three plain .md files in the agent's own namespace Phase 10 universal memory preamble, correction capture, weekly reflection Phase 11 real agent.create; deleted files.createAgent (it made a FOLDER) Phase 13 duplicate / publish / unpublish + file-grant enforcement Phase 14 one-level agent folders, seeded from the templates Migrations applied to the shared DB (additive, idempotent): document_updates_actor_agent.sql, chat_thread_agent_binding.sql Corrections found by running the real path: - the ambient scope the doc named (tool-call-timing) carries only a clock, no identity; added agent-run-scope.ts and opened it in runAutomationInProcess. - the correction-pair shape in the doc could never match: capture opens a new session at version+1, so from_seq is never equal to the prior to_seq. - agent.create's default AOP picked an empty routing bucket with no PLAYBOOK.md, so the first source edit failed. Ordering now happens in SQL — comparing the counts in JS made '2' > '17'. - touched no longer double-lists what owned already shows. 401 backend tests green. Test data created while driving this (4 agents, 3 memory files, 2 chat threads) removed and verified gone. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 8:23 PMuse-global-canvas-presets.tsrefactor(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 PMuse-grouped-conversations.tsperf(crm): give conversation rows their own event projection A row draws a dot timeline and three "last <type> at" columns, and nothing else — but crm.listConversations was building up to seven nested sub-objects per event through eight LEFT JOINs to feed it. Rows now get five scalars and a flat threadId, ~3x smaller per event, so the cap can move 15 -> 60 and draw three times the history for no more bytes: measured on a 50-row page, 547KB over 641 events before, 538KB over 1,919 now. That cap was also quietly wrong. "Last meeting" was derived by scanning whatever events the list happened to ship, so a deal whose last meeting fell outside its 15 most recent rendered blank — 53 of the 304 conversations with a meeting, in the org this was measured on — while SORTING by the same field used an unbounded MAX(occurred_at) and placed it somewhere the column contradicted. The three columns now read scalars computed over the whole history, from the same expressions the ORDER BY builder already used. Splitting the projections needed guards the single shared type had been hiding: - setConversations takes an explicit `projection`. Both queries write one store key and replace wholesale, so a list refetch would otherwise empty the timeline of whatever deal was open — the same bug that previously just shrank it to 15 (client-data-architecture.md §1.2). - getEventType/getEventTitle read `eventType` rather than sniffing which sub-object came back from a join, which equated "this join returned nothing" with "this is a note" and would have greyed out every dot. - a dot click opens EventDetailDialog on an id and fetches the full event, since the row no longer carries one. Also carries the in-flight pending-conversation-field-writes mask and its tests: it edits the same setConversations body, so it cannot be split out. Co-Authored-By: Claude Opus 5 <<email>>Sep 2, 2026, 11:01 PMuse-optimistic-conversation-actions.tsperf(crm): give conversation rows their own event projection A row draws a dot timeline and three "last <type> at" columns, and nothing else — but crm.listConversations was building up to seven nested sub-objects per event through eight LEFT JOINs to feed it. Rows now get five scalars and a flat threadId, ~3x smaller per event, so the cap can move 15 -> 60 and draw three times the history for no more bytes: measured on a 50-row page, 547KB over 641 events before, 538KB over 1,919 now. That cap was also quietly wrong. "Last meeting" was derived by scanning whatever events the list happened to ship, so a deal whose last meeting fell outside its 15 most recent rendered blank — 53 of the 304 conversations with a meeting, in the org this was measured on — while SORTING by the same field used an unbounded MAX(occurred_at) and placed it somewhere the column contradicted. The three columns now read scalars computed over the whole history, from the same expressions the ORDER BY builder already used. Splitting the projections needed guards the single shared type had been hiding: - setConversations takes an explicit `projection`. Both queries write one store key and replace wholesale, so a list refetch would otherwise empty the timeline of whatever deal was open — the same bug that previously just shrank it to 15 (client-data-architecture.md §1.2). - getEventType/getEventTitle read `eventType` rather than sniffing which sub-object came back from a join, which equated "this join returned nothing" with "this is a note" and would have greyed out every dot. - a dot click opens EventDetailDialog on an id and fetches the full event, since the row no longer carries one. Also carries the in-flight pending-conversation-field-writes mask and its tests: it edits the same setConversations body, so it cannot be split out. Co-Authored-By: Claude Opus 5 <<email>>Sep 2, 2026, 11:01 PMuse-working-memory.tsrefactor(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 PM