CedarCopilot wants to merge 2 commits into staging from fix/existing-drafts-thread-enumeration-20260906-141203
Live on prod, 1 of 2 behaviors degraded
Strict CloudWatch fallback found 23 prod failure log lines for draft.thread_id_unrecoverable (Requested entity was not found.) in the last 24h while checking getconversation, analyticsdraftactions, threadid, emailmessages, conversations.hydrate-slack.test.
Surface getConversation was already failing at 5/5 (100%) in the 71h before this deploy and 30/57 (52.6%) after, so this is a pre-existing condition, not evidence against PR #2697. The intent could not be verified from failure-only span evidence.
Libra has verdicts on 1 of 2 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.
A draft never becomes a CRM event (drafting.ts writes one on every keystroke-ish save , our own unsent text is not an event). That means a thread holding only a Cedar-created draft, with no prior real correspondence, has no CRM event on it , and getConversation()'s thread enumeration (uniqueThreadIds) is built exclusively from conversation.events. So a draft-only thread was never fetched from S3, never landed in emailMessages, and therefore could never appear in the <existing_drafts> context block a drafting agent reads before writing a new one.
This is deterministic, not flaky , it reproduces on any account, every time, for a draft created on a brand-new thread.
Traced via Sherlock (2026-09-06) from two independent duplicate-draft reports:
drafter run created a second draft on an existing thread 86 minutes after the first, outside the dispatch-level REVIEW_SKIP_IF_RUN_WITHIN_HOURS window , and never attempted to check the thread at all.drafter run created a third fresh-thread draft for the same ask ("send the YETI capabilities deck") across three separate days. This run correctly tried to check , calling search-live-emails and read-live-email-thread on both known prior draft threads , and had the thread IDs from the conversation's own nextSteps field. But even setting aside a separate Missing userId in request context bug on that call path, the passive <existing_drafts> context block could never have shown the prior drafts anyway: crm_email_events has zero rows for either prior thread, confirmed both via Postgres and via CloudWatch/Axiom logs showing the general Gmail sync completing successfully while the CRM-enrichment step (which is what actually creates crm_email_events rows) explicitly excludes "gained draft" messages by design.services/crm/conversations.ts , getConversation(): after building uniqueThreadIds from CRM events, also pull in thread IDs from analytics_draft_actions (joined to agent_executions for the conversation) where status = 'not_sent'. Every Cedar-created draft gets an unconditional row there (createDraftAnalytics, called from createDraftInProvider) regardless of CRM-event status, so this closes the gap without touching the CRM-event/enrichment design at all , those draft-only threads now flow through the existing S3-fetch/hydration path unchanged, and <existing_drafts> can see them.
Also fixed a test-mock gap this surfaced: conversations.hydrate-slack.test.ts's db stub didn't support .innerJoin(), so it broke on the new query. Added innerJoin alongside the mock's existing from/where/orderBy chain methods.
services/crm/__tests__/ suite passes (64 files / 767 tests, no regressions).getConversation() (or a drafter subagent) against a conversation with an unsent, fresh-thread Cedar draft and no other CRM event on that thread , emailMessages and the rendered <existing_drafts> block should now include it. Aspireiq's YETI conversation (368f00a2-4d32-4e0f-b3f1-df260b31bd83) is a live repro case: it has three such orphaned draft threads today.This fixes the passive-context half of the duplicate-drafting problem (both Zach's and Zak's cases share this root gap). Two related things are not covered by this PR and are worth separate follow-ups:
search-live-emails / read-live-email-thread fail fleet-wide with Missing userId in request context on this same dispatch path , a request-context construction bug, confirmed across at least 3 unrelated orgs in CloudWatch.🤖 Generated with Claude Code
Libra has not measured any production surfaces for this change yet.