Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(slack): add Send action button for drafted emails

merged#2821CedarCopilot

CedarCopilot wants to merge 3 commits into staging from feat/slack-send-email-action

What Libra verified

  • Draft threading and attachments survive the send path

    Libra ran this behavior against the change and confirmed the check detects when it breaks.

    Technical evidence
    • a fresh Gmail draft omits its message-self thread ID instead of treating the draft as a reply
    • a reply draft retains its existing Gmail thread ID for the outgoing message
    • draft attachments remain attached to the parsed draft data used by the send path

    Changed code: google.ts.

Live on prod, 3 of 9 surfaces working, less than a day leftTimeline and evidence
  1. Opened
    Sep 18, 2026, 10:15 AM
  2. Sep 18, 2026, 11:57 AM
  3. Merged
    Sep 18, 2026, 12:04 PM
  4. Live on prod
    Sep 18, 2026, 12:04 PM
  5. Observed 2 days, 9 surfaces, 1,202 requests
    Sep 18, 2026, 12:04 PM
  6. Pipelines steady after this deploy
    Sep 18, 2026, 12:04 PM
  7. Watching

    Live on prod, 3 of 9 surfaces working, less than a day left

    Sep 20, 2026, 12:02 PM

Behaviors Libra is checking

Sending a reply draft preserves its existing Gmail thread, while a fresh compose draft does not incorrectly carry its own message ID as a reply thread.Inconclusivelow confidence

Verification cited only 1 matching post-deploy event, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Sep 19, 2026, 10:48 AM
The Slack agent offers both Review Draft and Send buttons only for email drafts with at least one recipient, and omits Send for Slack drafts or recipient-less drafts.Inconclusivelow confidence

Verification cited only 1 matching post-deploy event, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Sep 19, 2026, 10:48 AM
After a successful send, the operation records the draft as sent with its user, draft, thread, recipients, subject, and body metadata.Inconclusivelow confidence

Verification cited only 1 matching post-deploy event, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Sep 19, 2026, 10:57 AM
Attachments fetched from the Gmail draft are forwarded to the send operation with their filenames, MIME types, sizes, and base64 bodies intact.Inconclusivelow confidence

Verification cited only 1 matching post-deploy event, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Sep 19, 2026, 10:57 AM
Concurrent Slack clicks for the same user and draft are serialized so only one attempt can send and competing clicks receive a retry-later response.Inconclusivelow confidence

Verification cited only 1 matching post-deploy event, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Sep 20, 2026, 1:46 AM
Every Slack send_email_draft button displays a native confirmation dialog before the irreversible send, including a safe default dialog when the agent omitted confirmation metadata.Inconclusivemedium confidence

Since deploy, OTEL found no /slack/interactions or send_email_draft-specific spans. CloudWatch shows 4 Slack interaction handler executions (8 handler log lines) across 2 users, with no tied handler failures. One outbound structured response explicitly contained a.

prod, checked Sep 20, 2026, 1:58 AM

Libra measured 3 of 9 production surfaces on prod. 1 surface had under 20 requests, so Libra has not judged it; 4 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.

Summary

Adds a send_email_draft operation to the Slack button registry so a drafted email posted to Slack gets a one-click Send action alongside the existing "Review Draft" link button, gated by a native Slack confirmation dialog , sending is the first irreversible op in this registry.

See docs/design/slack-send-email-action.md for the full design writeup. Key points:

  • Fixed a real data-loss gap first: GoogleMailManager.parseDraft discarded Gmail's own message.threadId. Naively echoing it back isn't enough on its own , Gmail assigns every message a threadId, including a brand-new draft's only message (threadId === message.id there, since it's the thread's first message). A reply's threadId differs from its own message id. parseDraft now derives threadId only when it represents a genuine reply, matching the same distinction createDraft already relies on when writing.
  • send_email_draft reuses the existing sendDraft/getDraft/updateDraftSent/completeTaskByDraftId path rather than calling Gmail directly , it inherits assertProviderSideEffectsAllowed/withSuppressedDelivery for free. It's idempotent (checks analytics_draft_actions first; falls back to treating a getDraft 404 as "already sent," since that check is best-effort and Gmail deletes a draft immediately after sending), and refuses to send a draft with no recipient (drafting deliberately leaves to blank rather than guess one).
  • Slack's native confirm dialog renders client-side before any webhook fires , a decline never reaches the server, so there's nothing to log, and any dynamic confirm text has to be baked in at message-build time. Added to the button schema (and its two duplicates in notifyUserTool.ts/notifyTool.ts), plus a hardcoded default specifically for send_email_draft so sending is never one click away regardless of what the agent's JSON contains.
  • Updated the Slack bot's prompt so a drafted email gets both buttons when it has a recipient, and gave the agent the draft's own ID (listTasksTool.ts) , without it there was no value to put in the button's args.draftId.

What changed, by phase

  1. ParsedDraft.threadId fix in the driver (lib/driver/google.ts, lib/driver/types.ts).
  2. New send_email_draft op in operation-registry.ts.
  3. confirm schema field + Block Kit rendering + hardcoded default (headless-response-schemas.ts, notifyUserTool.ts, notifyTool.ts, slack-block-kit.ts).
  4. Prompt updates (slack-bot-chat.ts, operation-registry.ts's agent_prompt example, listTasksTool.ts).
  5. A generic copy fix in slack-interactions.ts ("Click the button to retry" instead of a hardcoded "Click Accept," which was actively wrong for this op's "Send" label).
  6. Tests: the op's happy path / idempotent short-circuit / empty-recipient rejection / no-connection failure / 404-fallback; the driver's reply-vs-fresh threadId derivation; Block Kit confirm rendering + default injection.

Explicitly not extended to iMessage tapbacks , a tapback is a single instant reaction with no confirm-dialog equivalent, so a "Send" tapback would be one fat-finger tap from an irreversible send with no gate at all.

Advisory findings (reviewed, judged non-blocking)

Libra flagged two things on the commit:

  • No end-to-end Slack-interaction-payload test. True, but cancel_task/snooze_task/agent_prompt have none either , only buildApprovalApplyPrompt (a pure function) is tested; handleSlackInteraction has no test file today. New tests match existing precedent (testing the op's execute directly).
  • invalid_grant handling on the new Gmail path. getDraft/sendDraft/create all route through the shared withGmailCallhandleOperationError wrapper (google.ts:5241-5249), which already checks invalid_grant generically for every Gmail call. This PR adds no new raw Gmail API surface , it calls the same existing send.ts functions every other send surface uses , so this handl
Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
/a8n/notify/google1024 → 5130 → 0 (0%)226 ms → 157 ms0Working
513 requests since the deploy with 0 errors (0 errors in the 1,024-request baseline before it). p95 157 ms, was 226 ms.
POST /a8n/notify/google296 → 1710 → 0 (0%)250 ms → 157 ms0Working
171 requests since the deploy with 0 errors (0 errors in the 296-request baseline before it). p95 157 ms, was 250 ms.
/a8n/notify/google-calendar131 → 360 → 0 (0%)135 ms → 59 ms0Working
36 requests since the deploy with 0 errors (0 errors in the 131-request baseline before it). p95 59 ms, was 135 ms.
gmail.users.threads.get318 → 4709 → 6 (1%)627 ms → 419 ms72Pre-existing
6 of 470 requests failed (1.3%) since the deploy, matching the 2.8% baseline error rate before it. Not attributable to this change.
POST /a8n/notify/google-calendar39 → 120 → 0 (0%)188 ms → 59 ms0Insufficient traffic
12 requests, under the 20 Libra needs
google_subscription_subscribe0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
execute_tool notify-user2 → 00 → 0 (0%)1030 ms → not measured0No traffic
No requests recorded since this deploy.
/api/trpc/recall.getCallSessionByGoogleEventId0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/auth/callback/google0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.

Changed files → surfaces

  • apps/server/src/lib/driver/google.ts/a8n/notify/googlegmail.users.threads.getPOST /a8n/notify/google/a8n/notify/google-calendarPOST /a8n/notify/google-calendargoogle_subscription_subscribe+2
  • apps/server/src/mastra/tools/notify/notifyTool.ts/a8n/notify/googlePOST /a8n/notify/google/a8n/notify/google-calendarPOST /a8n/notify/google-calendarexecute_tool notify-user
  • apps/server/.claude/skills/cedar-configuration/surfaces-and-delivery.mdno production surface mapped
  • apps/server/src/lib/driver/__tests__/google-get-draft-thread-id.test.tsno production surface mapped
  • apps/server/src/lib/driver/types.tsno production surface mapped
  • apps/server/src/mastra/operations/__tests__/send-email-draft.test.tsno production surface mapped
  • apps/server/src/mastra/operations/headless-response-schemas.tsno production surface mapped
  • apps/server/src/mastra/operations/operation-registry.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/slack/slack-bot-chat.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/slack/slack-interactions.tsno production surface mapped
  • apps/server/src/mastra/tools/notify/notifyUserTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/listTasksTool.tsno production surface mapped
  • apps/server/src/services/notifications/__tests__/slack-block-kit.test.tsno production surface mapped
  • apps/server/src/services/notifications/slack-block-kit.tsno production surface mapped
  • docs/design/slack-send-email-action.mdno production surface mapped