Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(tasks): read draft cleanup pointer via task_output fallback

merged#2816CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/task-cleanup-draft-task-output-fallback

What Libra verified

  • Draft cleanup invocation is visible even without a deletable pointer

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

    Technical evidence
    • cleanupDraftForTask records the task identity even when the task has no email pointer
    • cleanupDraftForTask records the task identity when produced output is not email

    Changed code: tasks.ts.

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 17, 2026, 6:01 PM
  2. Sep 17, 2026, 6:16 PM
  3. Merged
    Sep 17, 2026, 8:41 PM
  4. Live on staging
    Sep 17, 2026, 8:41 PM
  5. Live on prod
    Sep 17, 2026, 8:41 PM
  6. Observed 2 days, 8 surfaces, 91 requests
    Sep 17, 2026, 8:41 PM
  7. Pipelines steady after this deploy
    Sep 17, 2026, 8:41 PM
  8. Pipelines steady after this deploy
    Sep 17, 2026, 8:41 PM
  9. Unobserved

    Live on prod, no production signal yet

    Sep 20, 2026, 6:04 PM

Behaviors Libra is checking

After Gmail confirms a Cedar-authored draft was deleted, the server emits a draft deletion analytics span and structured log with the user, draft, thread, and delete action.Brokenlow confidence

Verification reached its budget before a verdict.

staging, checked Sep 20, 2026, 8:04 PM
Failures while recording draft-deletion analytics are swallowed so they cannot turn an already successful Gmail deletion into a failed task cleanup.Degradedlow confidence

Unable to complete the verification because the telemetry query was rejected by the read-only query tool before returning data.

prod, checked Sep 20, 2026, 5:03 PM
After a draft is deleted, the task retains only its thread reference while task_action_data and task_output are both written in synchronized email-pointer forms without the deleted draft identifier.Degradedlow confidence

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

prod, checked Sep 20, 2026, 6:04 PM
cleanupDraftForTask now prefers an email draft pointer in task_output and falls back to task_action_data, allowing output-only tasks to delete Gmail drafts by draftId or emailHeaderMessageId while ignoring non-email outputs.Workingmedium confidence

Since the 2026-09-18T03:41:02Z deploy, CloudWatch shows 383 cleanupDraftForTask invocations, 182 draft-pointer selections, and 182 successful draft deletions across 14 worker streams. Every observed draft selection was followed by a successful deletion. The 55 tied failures.

prod, checked Sep 19, 2026, 10:12 AM
A confirmed Gmail draft deletion now emits a Cedar draft-deletion analytics record using the same draft identity and user attributes as create and send analytics.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 18, 2026, 4:37 PM
Every cleanupDraftForTask invocation now emits an explicit log entry, including invocations that find no deletable draft pointer.Inconclusivelow confidence

Verifier did not provide positive runtime evidence tied to this intent, so Libra is keeping it inconclusive. Original assessment: Since deployment, CloudWatch shows 354 explicit [cleanupDraftForTask] Invoked for task logs across prod: 275 in worker-service and 79 in.

prod, checked Sep 18, 2026, 5:39 PM

Failures attributed to this change

No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 17, 2026, 11:29 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 18, 2026, 1:33 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 18, 2026, 2:04 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 18, 2026, 2:18 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 18, 2026, 4:18 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 2 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 18, 2026, 9:18 AM

Libra measured 2 of 8 production surfaces on prod. 2 surfaces had under 20 requests, so Libra has not judged them; 4 surfaces had no requests at all. Libra has 2 low-confidence degraded verdicts it is still confirming. Libra checks hourly for 3 days after each deploy.

Bug

When a task representing an outstanding email draft is marked done with cleanupDraft: true (the user fulfilled the obligation another way, e.g. sent an equivalent email manually, so the now-stale Cedar-generated Gmail draft should be deleted), cleanup silently no-ops for a large class of tasks and the real Gmail draft is never deleted.

cleanupDraftForTask() (apps/server/src/services/user-tasks/tasks.ts) and its gating check in updateTaskTool.ts's update-task tool both read draftId/emailHeaderMessageId/threadId , and gate entirely , on the deprecated task_action_data column (taskActionData?.channel === 'email'). Since the Aug-2026 migration, task_output is the canonical location for a produced task's draft pointer and task_action_data is null on the common/default case (see apps/server/src/services/user-tasks/output-predicates.ts, which already reads via COALESCE(task_output->>'draftId', task_action_data->>'draftId') for exactly this reason). cleanupDraftForTask/updateTaskTool.ts never got updated to that pattern, so:

  • deleteDraft() is never called for a task whose pointer lives in task_output
  • update-task still reports { success: true }
  • in updateTaskTool.ts, the gate also fed the "does this write anything real?" no-op check, so a cleanupDraft: true call on an already-done task (nothing else changing) was short-circuited before cleanupDraftForTask was ever invoked
  • the stale Gmail draft is left behind indefinitely

Confirmed on a real production task (bf7bd732-46db-4654-97b2-3441965add9f, Zach Moskow / ConcentrateAI): task_action_data IS NULL, task_output = {"kind":"email"}, and a real Gmail draft (r4440516407368735025) created 5 minutes before the task was marked done is still sitting undeleted.

Fix

  • cleanupDraftForTask(): read draftId/threadId/emailHeaderMessageId with the same task_output-first, task_action_data-fallback pattern output-predicates.ts already uses for SQL reads, instead of task_action_data exclusively.
  • updateTaskTool.ts's cleanupDraft gate: same fallback, so the no-op short-circuit can no longer swallow the call before cleanup runs. Both axes (taskActionData + taskOutput) are written back in sync via the existing producedOutput helper, matching the paired-write convention clearTaskDraftPointer already follows.
  • Observability, so this exact investigation dead end can't recur:
    • An unconditional [cleanupDraftForTask] Invoked for task … log at entry, so a run's logs always show whether cleanup was ever called, independent of outcome.
    • A new "delete" cedar.draft.action value, emitted via a new recordDraftDeleted() in services/analytics/draft-analytics.ts from the successful-deletion path, following the exact same cedar.draft.* span/attribute pattern already used for "create"/"send".
  • Tests: added coverage in cleanup-draft-pointer.test.ts (draft pointer living only in task_output, emailHeaderMessageId-only, task_output-over-stale-task_action_data precedence, non-email task_output.kind, and the new recordDraftDeleted emission) and in updateTaskTool.test.ts (a cleanupDraft: true call on an already-done task whose pointer lives only in task_output still triggers cleanup instead of being swallowed by the no-op guard).

Verification

  • pnpm --filter @zero/server run types , clean
  • pnpm --filter @zero/server exec vitest run src/services/user-tasks/__tests__/cleanup-draft-pointer.test.ts src/mastra/tools/task/__tests__/updateTaskTool.test.ts , 73/73 passing
  • pnpm deps:check , no dependency violations
  • Reviewed the diff against thermo-review's standards (dependency direction, file-size, false invariants, spaghetti/branching, reuse of existing helpers) , no blocking findings; see PR discussion for the full pass.

Out of scope

  • Deleting the actual live stale draft in Zach's real Gmail account (separate manual/support action).
  • The orphaned-execution-id investigation (
Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
model_chunk Next Steps and Tasks Manager136 → 470 → 0 (0%)1 ms → 7 ms0Working
47 requests since the deploy with 0 errors (0 errors in the 136-request baseline before it). p95 7 ms, was 1 ms.
model_step Next Steps and Tasks Manager67 → 270 → 0 (0%)32078 ms → 29059 ms0Working
27 requests since the deploy with 0 errors (0 errors in the 67-request baseline before it). p95 29059 ms, was 32078 ms.
invoke_agent Next Steps and Tasks Manager33 → 150 → 0 (0%)65219 ms → 57461 ms0Insufficient traffic
15 requests, under the 20 Libra needs
/api/trpc/userTasks.listUserTasks6 → 30 → 0 (0%)6248 ms → 724 ms0Insufficient traffic
3 requests, under the 20 Libra needs
/api/trpc/userTasks.getTaskById0 → 30 → 0 (0%)not measured → 101 ms0Insufficient traffic
3 requests, under the 20 Libra needs
execute_tool list-tasks2 → 20 → 0 (0%)63 ms → 292 ms0Insufficient traffic
2 requests, under the 20 Libra needs
POST /api/trpc/userTasks.listUserTasks4 → 20 → 0 (0%)6248 ms → 724 ms0Insufficient traffic
2 requests, under the 20 Libra needs
cedar.analytics.draft.delete0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
cedar.analytics.draft.delete0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/userTasks.listUserTasks0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
POST /api/trpc/userTasks.listUserTasks0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/userTasks.getTaskById0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
model_chunk Next Steps and Tasks Manager16 → 00 → 0 (0%)1 ms → not measured0No traffic
No requests recorded since this deploy.
model_step Next Steps and Tasks Manager7 → 00 → 0 (0%)18588 ms → not measured0No traffic
No requests recorded since this deploy.
invoke_agent Next Steps and Tasks Manager1 → 00 → 0 (0%)24843 ms → not measured0No traffic
No requests recorded since this deploy.
execute_tool list-tasks1 → 00 → 0 (0%)459 ms → not measured0No traffic
No requests recorded since this deploy.

Changed files → surfaces

  • apps/server/src/services/user-tasks/tasks.tsmodel_chunk Next Steps and Tasks Managermodel_step Next Steps and Tasks Managerinvoke_agent Next Steps and Tasks Manager/api/trpc/userTasks.listUserTasks/api/trpc/userTasks.getTaskByIdexecute_tool list-tasks+8
  • apps/server/src/mastra/tools/task/__tests__/updateTaskTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/updateTaskTool.tsno production surface mapped
  • apps/server/src/services/analytics/draft-analytics.tsno production surface mapped
  • apps/server/src/services/analytics/draft-trace-attrs.tsno production surface mapped
  • apps/server/src/services/user-tasks/__tests__/cleanup-draft-pointer.test.tsno production surface mapped