Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(drafting): let the drafting executor see mail that never synced

merged#2649CedarCopilot

CedarCopilot wants to merge 2 commits into staging from worktree-fix-drafter-live-mail-tools

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 1, 2026, 10:23 PM
  2. Sep 1, 2026, 10:40 PM
  3. Merged
    Sep 1, 2026, 10:50 PM
  4. Live on prod
    Sep 1, 2026, 10:50 PM
  5. Observed 0 hours
    Sep 1, 2026, 10:50 PM
  6. Unobserved

    Live on prod, no production signal yet

    Sep 1, 2026, 10:50 PM
  7. Pipelines steady after this deploy
    Sep 1, 2026, 10:50 PM

Behaviors Libra is checking

On-event drafting executions can search the connected user's live Gmail mailbox when synced-email search has no results, including mail that never synced into Cedar.Not checked
prod
On-event drafting executions can read a full live Gmail thread identified by a live-mail search so drafts can be grounded on the real prior conversation.Not checked
prod

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.

The bug

ON_EVENT_DRAFTING_EXECUTOR_TOOLS granted find-emails, which queries crm_email_events (Cedar's synced store) and nothing that can reach the live mailbox. On a deal whose history predates the user's sync window that returns nothing, so the drafter has no thread to reply on and no contact to address.

That is the normal case for a reactivation, not a corner. For <email>, 90 of 145 closed-lost deals hold no synced email at all, and Cedar holds exactly one thread for 34 of the 50 deals his reactivation drafts landed on , often a calendar invite, a bounce, or a Superhuman reminder, because that is the only message that survived the sync window.

Notably this is not a thread-selection bug: all 34 drafts that anchored onto a pre-existing thread chose the newest thread Cedar holds. Zero picked an older one. The selection logic is fine; the catalogue it selects from is a coverage artifact.

The agent went looking anyway. In one day, 13 of 30 drafting runs hand-rolled Gmail API curls through run-code-executor , the wrong surface (not suppressed by --no-send) and unreliable:

  • Otamiser: three curl attempts, recovered thread 19b2dc840e62339b, drafted correctly.
  • Cortex: asked HubSpot for a Gmail thread id (not a thing), fabricated a 32-char value (real ids are 16), got 400 Invalid thread_id value, and the retry dropped threading entirely and composed a new email subjected Re: Invoice Butler // Cortex.

search-live-emails and read-live-email-thread already existed and were already registered in the conversation skill. They had simply never been reachable from any background path: zero calls fleet-wide in 60 days, against 1,400+ for find-emails.

The fix

Two ids added to the allowlist. No prompt change, no playbook change.

Evidence

live-thread-search-eval.ts, 5 trials per cell, record-only stubs (no DB, no Gmail, no drafts). Two fixtures pulling in opposite directions: Otamiser rewards searching, Cortex punishes fabricating.

Otamiser: found the real threadCortex: abstainedfell back to code-exec
A , shipped grant0/55/50/5
B , + live mail tools (this PR)5/54/51/5
C , B + a "ground or abstain" prompt rule5/52/54/5

Variant C was tested and rejected. Telling the agent to ground its evidence made it escalate to run-code-executor when a search came back empty, then draft anyway , abstention fell from 4/5 to 2/5. email.md already says "NEVER guess or make up thread IDs" and production fabricated one regardless, so adding prose is not the lever. The change worth making there is subtractive (the recipient rule's automated-execution carve-out) and is written up in the plan doc rather than made here.

What this does NOT do

  • Does not repair the 50 existing bad drafts. docs/customer-feedback/2026-09-01-mihir-reactivation-draft-repair.md is the re-anchoring plan for those.
  • Does not prove thread-selection correctness on noisy results. The eval stubs a single clean hit; picking the right thread out of eight candidates is untested and is the likeliest remaining failure.
  • Does not reproduce the production fabrication. Variant A abstained in every trial, so the eval shows the fix does not cause fabrication, not that it stops it. Likely because the harness uses SKILL.md rather than the assembled DRAFTING_INSTRUCTIONS, so it never carried the existing "NEVER guess" rule.

Verification

  • New test mutation-checked: deleting search-live-emails from the allowlist turns it red.
  • pnpm --filter @zero/server run types clean. types:test shows only pre-existing failures, none in the new file.
  • Post-deploy: cedar-cli subagent replay --no-send against a7b28d83-f159-47c2-950e-18bfd61b8ef2 (Cortex) and 091678cb-89d7-4f5c-acc3-7faa24e22e7d (Otamiser). Expect the thread to come from search-live-emails rather than run-code-executor.

Note on fixtures

The

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/live-thread-search-eval.tsno production surface mapped
  • apps/server/src/mastra/tools/__tests__/drafting-executor-live-mail-grant.test.tsno production surface mapped
  • apps/server/src/mastra/tools/subagent-tool-allowlists.tsno production surface mapped
  • docs/customer-feedback/2026-09-01-mihir-reactivation-draft-repair.mdno production surface mapped