CedarCopilot wants to merge 1 commit into staging from chore/f8-step4-vendor-bucket-repair
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
Not executed. Dry-run reviewed and attached below. This renames live conversations, so it needs a human go/no-go before APPLY=1.
A conversation's primary company domain is the vendor's (fathom.video) while its NAME is a customer's ("Broadband Hospitality") and it carries a deal stage. Both cannot be true. The domain is the honest one: these conversations contain zero email from the company they are named after.
That incoherence is precisely what stops findOrCreateVendorConversation distinguishing a bucket from a deal when both carry the vendor's domain, so repairing it is what lets Step 3 use a plain domain lookup. See docs/design/f8-vendor-conversation-routing.md.
Per conversation: name → the vendor, status → NULL, aopId → the user's no-op Vendor notification AOP.
Both are now predicates with tests, because I got the selection wrong twice.
1. Email-only evidence is not sufficient. A genuine deal can have every EMAIL be a Circleback or Fathom recap while the real relationship lives in meetings. Of 98 email-only matches:
| count | |
|---|---|
| had meetings | 15 |
| had outbound | 18 |
| were closed_won | 18 |
Requiring the conversation to be inert (no meeting, no outbound event) drops those.
2. A booked outcome is never repaired. 23 inert matches carried won/lost/closed, including a closed_won whose only trace is 90 DocuSign notices. That is the shape of a deal worked off-channel where only the signature flow reached email, and nulling its stage would remove a real win from the pipeline.
98 → 69 → 46 conversations across 16 users. Also asserts no linked external CRM deal.
The rename discards the old conversation name. Every old → new pair prints before any write, and the dry run is the record. Events, tasks and history are untouched.
The 23 booked-outcome conversations stay incoherent by design. So a domain-only lookup in Step 3 would still adopt them, which means Step 3 needs a residual discriminator regardless of this repair. Worth deciding before Step 3 is built.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NVHnt2pEEjhVwSyhkcaxoC
This PR adds dry-run-by-default maintenance tooling to identify vendor-dominated conversations, rename eligible records as vendor buckets, clear their deal status, and optionally assign a no-op AOP. It also adds selection-unit tests, executable bundling wrappers, and extensive design/incident documentation.
This PR is not safe to merge or execute until the unintended cleanup invocation, destructive selection errors, race protection, and production-data exposure are resolved.
Running the repair currently executes both maintenance scripts, while raw-status outcome detection and unanchored sender matching can erase legitimate deal state or tasks; the ID-only writes can also act on conversations that became protected after selection, and the added public documentation exposes identifiable production records.
Files Needing Attention: apps/server/scripts/repair-vendor-bucket-conversations.ts, apps/server/scripts/cancel-vendor-bucket-tasks.ts, docs/design/f8-vendor-conversation-routing.md, docs/design/stale-task-failure-modes.md, docs/design/refused-save-client-state.md
The ad
Libra has not measured any production surfaces for this change yet.