Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix: next-steps backstop double-fires on provider-shape false negative

closed#2805CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/next-steps-backstop-db-check

Not deployedTimeline and evidence
  1. Opened
    Sep 16, 2026, 8:10 AM
  2. Sep 16, 2026, 8:14 AM
  3. Merged, live on staging, live on prod, observed
    Pending
  4. Not deployed

    Not deployed

    Pending

Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.

What was wrong

runNextStepsBackstop (apps/server/src/mastra/workflows/event-execution/on-event-agent-execution-workflow.ts) decided whether to dispatch update-next-steps-and-tasks based solely on orchestratorCalledNextSteps: didCallNextSteps(result) , an in-memory read of the orchestrator's generate() result. didCallNextSteps's own docstring already admits the tool-call shape "differs between provider adapters and neither one is guaranteed."

Real production evidence (Weiver customer, conversation d9b0de02-1208-4fa9-9272-b62d65a32cb9, run cd603b06-2b49-4c1f-9729-e780d658db71): the orchestrator DID call update-next-steps-and-tasks directly at 2026-09-13 10:02:30, creating task c6f051d2-3eea-4ae4-99b8-7933cceba95c. 39 seconds later, runNextStepsBackstop fired anyway , its guard evaluated orchestratorCalledNextSteps as false, a false negative , creating a second, overlapping duplicate task 876c8405-13b4-436b-ac59-9b89bdeb7e0c with contradictory framing (one calls itself the deal's "first" follow-up, the other references "a second email"). Both tasks trace to the same run_id.

What changed

Added didDispatchNextStepsInDb(runId) , a query against agent_tool_calls (run_id, tool_name) , as a second, authoritative signal, OR'd defensively with the existing in-memory check inside runNextStepsBackstop. It only runs when the in-memory check reads false, and can only suppress a false-positive backstop fire; it introduces no new failure mode since the original check is untouched.

The original docstring on didCallNextSteps warns that querying agent_tool_calls at that call site would race with the async tool-call logger. This is a different call site: by the time runNextStepsBackstop's guard runs, several other steps (including save-execution-summary) have already completed in the same run , consistent with the observed 39-second gap in the Weiver case , so the write is very likely to have landed.

Added a createStructuredLog('info', '[orchestratorAgentStep] next-steps backstop suppressed by DB check', ...) line for when the DB check is what caught the false negative, distinguishing "in-memory check works" from "in-memory check is broken and the DB check saved us" , that tells whoever reads the logs later whether didCallNextSteps itself still needs a deeper fix.

Also added unit tests for the new didDispatchNextStepsInDb (exported, mocking createDb the same way apps/server/src/mastra/tools/__tests__/tool-grant.test.ts does), covering both the row-found and no-row branches.

How to verify

  • apps/server/src/mastra/workflows/event-execution/__tests__/next-steps-backstop.test.ts , 9 tests passing, including the two new didDispatchNextStepsInDb cases.
  • In Axiom (cedar-prod), watch for [orchestratorAgentStep] next-steps backstop suppressed by DB check , any occurrence means the in-memory didCallNextSteps check produced a false negative that the DB check caught. If this line trends non-zero, didCallNextSteps itself likely needs a deeper fix (e.g. covering more provider adapter shapes) rather than relying on the DB check as a permanent crutch.
  • Absence of new duplicate-task pairs on the same run_id for outbound-send executions with open tasks (cross-reference user_tasks.creation_run_id for duplicates).

šŸ¤– Generated with Claude Code

https://claude.ai/code/session_019jTe7TuDHKYaFL6Ny2Z5h6

Greptile Summary

This PR adds a database-backed second signal to prevent the next-steps backstop from dispatching duplicate work when provider-specific result shapes defeat the in-memory tool-call check.

  • Queries agent_tool_calls by run ID and tool name when the in-memory check is negative.
  • Logs when the database signal suppresses the backstop.
  • Adds focused tests for matching and absent database rows.
  • The lookup currently introduces an unhandled database-failure path into
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/workflows/event-execution/__tests__/next-steps-backstop.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/event-execution/on-event-agent-execution-workflow.tsno production surface mapped