CedarCopilot wants to merge 2 commits into staging from fix/task-complete-draft-send-notify
Live on prod, no production signal yet
Post-deploy telemetry shows 19 successful updateNextStepsAndTasksTool completions, including 2 client-sync executions in api-service, with no tied tool failures observed. However, telemetry does not expose the request context payload, so there is no positive runtime evidence.
Unable to complete verification: the available telemetry queries returned no matching OTEL spans or CloudWatch log executions for handleExecuteFromClientSend after deployment, and source mapping to the concrete HTTP route could not be read because the source-read budget was.
Unable to complete verification because the source read budget was exhausted before telemetry could be queried.
Since 2026-08-29T19:45:37Z, production telemetry contains 0 OTEL spans or CloudWatch log entries tied to findRecentlyCompletedTaskByDraftId, handleExecuteFromClientSend, taskCompleted, or completeTaskByDraftId. The only related evidence was 278 thread-based.
Since 2026-08-29T19:45:37Z in prod, CloudWatch returned 0 matching records for [handleExecuteFromClientSend], Streaming taskCompleted, Marking email tasks complete, and draft-path closure logs. OTEL returned 0 explicit client-send/handler matches; broadened queries found only.
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.
Sending a Cedar-drafted email closes its task correctly , and then tells nobody.
Two writers race on the same send:
mail.send ──► completeTaskByDraftId ──► completeTask() writes 'done', streams nothing
client ──► handleExecuteFromClientSend
└─► markEmailTasksCompleteByThreadId writes AND streams taskCompleted
The draft path runs inside the send request; the handler only after the client fires a
follow-up call. So the draft path lands first, the handler's status = 'todo' predicate
matches nothing, and the one path that emits taskCompleted has nothing to emit it about.
Traced on a real send (<email>, task 979d93bb):
20:01:44.000 user sends the Cedar draft
20:01:46.097 completeTaskByDraftId writes status='done' ← no stream, no invalidation
20:01:46.142 [completeTask] Completed task 979d93bb...
20:01:46.708 handleExecuteFromClientSend: 0 rows matched ← streams nothing
20:01:57.618 [completeTask] ...is already done ← user clicks the checkbox
20:02:38.777 [completeTask] ...is already done ← user clicks again
Two consequences. The user watches an unchecked box on work that finished two seconds
earlier , and clicking it does nothing visible either, because completeTask early-returns
on an already-done task. And the next-steps agent is handed No task was programmatically closed about a task that had just closed; since activeTaskStatusConditions() hides done
rows from <existing_tasks>, a live next-steps bullet with no visible task behind it reads
as uncovered work, and the agent re-derives the task the send just completed.
findRecentlyCompletedTaskByDraftId (services/user-tasks/tasks.ts) , read-only
lookup of the closure the draft path already made. It reports a closure, never performs
one. Bounded by recency so re-sending an old draft cannot re-announce an old completion.handleExecuteFromClientSend now reports the union of both closure sets and streams
taskCompleted for all of them. draftId was already on the input schema
(handleExecuteFromClientSend.ts:40) and already forwarded by the composer
(email-composer.tsx:1456) , it was simply never read.triggerEventFacts is now set on this path, so buildTriggerEventBlock states the
closures instead of falling back to free-text reasoning. Direction is outbound by
construction: this handler only runs because the user just sent something.No new plumbing , the client's taskCompletedProcessor already updates both surfaces
(conversation.userTasks via setConversations, userTasksSlice via updateTask).
Send an email from a Cedar-generated draft that has a task attached. The task should tick
in the UI on send, with no refresh. In the execution's prompt, <trigger_event> should now
name the closed task rather than saying No task was programmatically closed.
tsc -b on @zero/server , exit 0, cleanvitest run over routeHandlers/event-execution/__tests__, services/user-tasks/__tests__,
mastra/tools/task/__tests__ , 289 passed, 2 skippedpnpm deps:check , no violations (1572 modules, 8190 dependencies)Deliberately narrow. Two adjacent defects found during the investigation are not fixed here and need their own PRs:
handleExecuteSyncThread.ts:637 hand-rolls its sender check against the default
connection only, ignoring send-as aliases, so a user who sends from a second address has
~every outbound labelled inbound. determineEmailDirection + getEmailAliases already
exist for exactly this.markEmailTasksCompleteByThreadId is called direction-blind at
handleExecuteSyncThread.ts:547, so an inbound message closes response tasks the user
never answered. 820 such closes in the last 30 days.🤖 Generated with Claude Code
Libra has not measured any production surfaces for this change yet.