Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(drafting): rewrite an existing draft in place instead of duplicating it

merged#2540CedarCopilot

CedarCopilot wants to merge 0 commits into staging from fix/draft-rewrite-in-place

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 21, 2026, 12:03 PM
  2. Merged
    Aug 21, 2026, 11:37 PM
  3. Live on staging
    Aug 21, 2026, 11:37 PM
  4. Live on prod
    Aug 21, 2026, 11:37 PM
  5. Observed 3 hours
    Aug 21, 2026, 11:37 PM
  6. Pipelines steady after this deploy
    Aug 21, 2026, 11:37 PM
  7. Pipelines steady after this deploy
    Aug 21, 2026, 11:37 PM
  8. Unobserved

    Live on prod, no production signal yet

    Aug 22, 2026, 2:21 AM

Behaviors Libra is checking

When an agent supplies an existing provider draft ID while editing a draft, Cedar updates that draft instead of creating a duplicate.Inconclusivelow confidence

OTEL fallback found no prod spans matching saveDraftTool → createDraftInProvider since deploy.

prod, checked Aug 22, 2026, 1:57 AM
Drafting agents can explicitly mark an email request as a rewrite by passing the draft thread ID through the draft-comms and save-draft tool interfaces.Inconclusivelow confidence

OTEL fallback found no prod spans matching draftCommsTool / saveDraftTool since deploy.

prod, checked Aug 22, 2026, 2:13 AM
If Cedar cannot look up the existing draft on a requested rewrite thread, it aborts the rewrite so a transient lookup failure cannot create a duplicate draft.Inconclusivelow confidence

OTEL fallback found no prod spans matching createDraftInProvider since deploy.

prod, checked Aug 22, 2026, 2:13 AM
When an agent asks to rewrite a draft on a named email thread, Cedar finds that thread's most recent existing draft and updates it in place rather than stacking another draft.Inconclusivehigh confidence

Since 2026-08-22T06:37:27Z, CloudWatch returned 0 logs for the required `[createDraftInProvider] Rewriting existing draft in place` signal, 0 logs for any `createDraftInProvider` variant, and 0 provider draft create/update log variants. OTEL draft-related results contained only.

prod, checked Aug 22, 2026, 2:20 AM
A named rewrite thread becomes the authoritative thread for draft lookup, reply threading, and recipient extraction, even if no thread ID or a conflicting thread ID was otherwise supplied.Inconclusivelow confidence

OTEL fallback found no prod spans matching createDraftInProvider since deploy.

prod, checked Aug 22, 2026, 2:21 AM
When an agent edits an existing email draft by provider draft ID or explicitly named thread, Cedar updates that draft in place instead of creating another draft.Inconclusivelow confidence

OTEL fallback found no prod spans matching draftCommsTool → saveDraftTool → createDraftInProvider since deploy.

staging, checked Aug 24, 2026, 11:52 AM

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

Problem

When a user asks the agent to edit an existing draft (in-app chat, a Slack DM to Cedar, iMessage/SMS, or external MCP), Cedar creates a new draft instead of updating the existing one , so you end up with two or three drafts on the same thread. Reported by Pirros (Xander hit it twice in one day → 3 drafts for one contact) and reproduced in prod telemetry (thread 1a01b1…, three scenario=new saves in one day).

Root cause

The chat/headless path , saveDraftTool (registered directly as save-draft on the master/headless surface) and its draftCommsTool wrapper , computed scenario = 'rewrite' but then called createDraftInProvider without forwarding either the provider draftId or the thread. With no existingDraftId, the service always hit Gmail drafts.create. The automated on-event path did it correctly (it resolves the existing draft via checkAndHandleExistingDraft and passes existingDraftId); only the chat/agent path had the gap.

Fix

Make the rewrite explicit and agent-driven: the agent names the thread whose draft to replace via a new rewriteDraftOnThreadId param (the thread_id it already sees in the <existing_drafts> context block and the task view). That threads through to createDraftInProvider, which resolves that thread's most-recent draft to its provider draft id and updates it in place , collapsing any existing duplicates. No ownership/staleness heuristics; the agent's explicit intent is the signal.

  • saveDraftTool / draftCommsTool: accept + forward rewriteDraftOnThreadId with a byte-identical agent-facing description on both (the headless surface calls saveDraftTool directly, so it needs the same guidance); also forward an explicit draftId as existingDraftId.
  • createDraftInProvider: resolve the thread's most-recent draft when asked, via a single thread fetch shared with the reply-all recipient lookup.
  • The on-event path is untouched , it already passes existingDraftId, so the new resolution is skipped for it.

Tests

saveDraftTool.test.ts , three cases locking both explicit signals (rewriteDraftOnThreadId and draftId) through to the service, and that a plain new draft forwards neither. draftCommsTool.test.ts , updated the field-forwarding expectation.

Verification

  • pnpm --filter @zero/server run types → clean (tsc -b, exit 0)
  • pnpm --filter @zero/server exec vitest run on both draft-comms suites → 33 passed
  • pnpm deps:check → no dependency violations
  • Reviewed with thermo-review; findings (redundant thread fetch, dead predicate, first-vs-latest draft, duplicated scenario logic, missing test) all addressed.

🤖 Generated with Claude Code

Greptile Summary

This PR adds an agent-supplied thread signal so chat and headless drafting paths can update an existing provider draft instead of creating duplicates.

  • Extends both draft tools with rewriteDraftOnThreadId and forwards explicit draft IDs.
  • Resolves an existing draft from thread details before invoking the provider save operation.
  • Adds tool-level forwarding and scenario tests.

Confidence Score: 3/5

The PR should not merge until rewrite resolution uses the explicitly named thread and aborts rather than creating a duplicate when that target cannot be resolved.

The changed service can ignore the named rewrite thread in favor of an independent threadId, and resolution failures fall through to the new-draft path, causing wrong-thread updates or duplicate drafts.

Files Needing Attention: apps/server/src/services/drafting/drafting.ts, apps/server/src/mastra/tools/draft-comms/saveDraftTool.ts

Important Files Changed

FilenameOverview
apps/server/src/mastra/tools/draft-comms/saveDraftTool.tsAdds rewrite scenario detection and forwards explicit draft and rewrite-thread identifiers, but leaves those identifiers indepen
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/src/mastra/tools/draft-comms/__tests__/draftCommsTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/draft-comms/__tests__/saveDraftTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/draft-comms/draftCommsTool.tsno production surface mapped
  • apps/server/src/mastra/tools/draft-comms/saveDraftTool.tsno production surface mapped
  • apps/server/src/services/drafting/__tests__/create-draft-rewrite-in-place.test.tsno production surface mapped
  • apps/server/src/services/drafting/drafting.tsno production surface mapped