Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(crm): route signature events to the counterparty, not the vendor bucket

merged#2694CedarCopilot

CedarCopilot wants to merge 3 commits into staging from feat/f8-route-signature-events-to-counterparty

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 6, 2026, 12:44 PM
  2. Sep 6, 2026, 1:57 PM
  3. Merged
    Sep 6, 2026, 5:29 PM
  4. Live on prod
    Sep 6, 2026, 5:29 PM
  5. Observed 2 days
    Sep 6, 2026, 5:29 PM
  6. Pipelines steady after this deploy
    Sep 6, 2026, 5:29 PM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 9, 2026, 12:49 PM

Behaviors Libra is checking

E-signature and document-notification events are routed to the CRM conversation for the counterparty named in the document title or body instead of the e-signature vendor’s conversation.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 9, 2026, 12:48 PM
When an e-signature event names a counterparty but no matching counterparty conversation exists, the event falls back to the vendor bucket without inventing a counterparty domain or opening a new deal.Inconclusivelow confidence

Verification showed no counted post-deploy activity, so Libra has no positive runtime evidence for how much traffic exercised this change. Original assessment: Post-deploy CloudWatch telemetry shows repeated concrete e-signature routing through assignEventToConversation.

prod, checked Sep 9, 2026, 12:49 PM

Libra has verdicts on 0 of 2 tracked behaviors on prod; 2 are still being checked. Libra checks hourly for 3 days after each deploy.

Step 5 of the F8 workstream. In the design doc this is docs/design/f8-vendor-conversation-routing.md §4.3 "Phase 3: route signature events to the counterparty"; its verification list (§6.4) is exactly the eval below.

The problem, and the mechanism

domain-identification.ts told the agent that every automated notification goes to the sender's vendor bucket , "do NOT open a deal, even if the body names a company." That rule is correct for a meeting recap (Cedar already has the meeting from calendar sync, so the company in the subject adds nothing) and wrong for a signature/transaction event, which reports a contract state change Cedar cannot learn from calendar, email or CRM sync.

What made it fail is the tool's argument order, not the agent's judgment. lookupCompanyFuzzyTool resolves domain first and returns on a hit (lookupCompanyFuzzyTool.ts:357-408), so passing the vendor's domain hands back the vendor's own bucket before the counterparty name is ever tried.

  • Baseline passes { domain: 'pandadoc.net', companyName: null } → the PandaDoc bucket.
  • Candidate passes { companyName: '<counterparty>', domain: null } → the counterparty's deal.

In a real production trace the agent called the tool with companyName: null on an email about a specific customer's order form and returned the PandaDoc bucket. 38 select-primary-company calls resolved that way.

What changed

apps/server/src/mastra/skills/crm/domain-identification.ts, instructions only:

  1. Classify before routing. An automated message is sorted by what it reports , a meeting artifact (recap, transcript, shared recording, agenda digest) or a contract-document state change , with the vendor's identity explicitly not deciding it, and recap as the default when it isn't unambiguously a document.
  2. Transaction path: read the counterparty out of the document title, pass it as companyName with domain: null.
  3. Fallback is mechanical, not composed. On a miss the agent calls the tool a second time with the sender's root domain and returns what comes back, instead of writing a domain from memory. This is what fixed the fallback case.
  4. Two general prohibitions, strengthened in place rather than duplicated: a company name is not evidence of a domain, and the user's own domain is only ever valid for a genuinely internal thread.

No code paths changed. pnpm --filter @zero/server run types clean, pnpm deps:check clean (1751 modules, no violations).

Results

playbook-instruction-eval, model gpt-4o (the real workload model). Baseline is this file as it stands on origin/staging , verified byte-identical to the frozen fixture, so the A/B isolates only this diff. Counts are aggregated over repeat 8-trial runs on identical fixtures.

Tuned set , visible while the instructions were being edited:

casebaselinecandidate
pandadoc-calderon4/1613/16
docusign-merrowbank , the case a regex fails0/1615/16
pandadoc-ashcombe-nomatch , no counterparty exists15/1616/16
otter-volante , recap control4/1616/16
warm-intro-northcloud , pre-existing case16/1616/16
one-way-intro-orivance , pre-existing case5/86/8

Held out , written from production before any iteration, scored afterwards, two signature vendors this change was never tuned against:

casebaselinecandidate
hellosign-formwork , counterparty named first0/3221/32
ironclad-kestrelbank0/1616/16
fellow-marlowe-recap , recap control15/1616/16

The unit of analysis is the case, not the trial. Trials within a case are repeats of one prompt, so they are not independent; there is no pooled N here and no p-value on n

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/.claude/skills/playbook-instruction-eval/domain-identification-eval.tsno production surface mapped
  • apps/server/.claude/skills/playbook-instruction-eval/fixtures/domain-identification-baseline.mdno production surface mapped
  • apps/server/.claude/skills/playbook-instruction-eval/fixtures/domain-identification-cases.tsno production surface mapped
  • apps/server/src/mastra/skills/crm/domain-identification.tsno production surface mapped