Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(tasks): promote covering recommendations to todos instead of duplicating

closed#2695CedarCopilot

CedarCopilot wants to merge 4 commits into staging from fix/recommended-todo-duplicate-tasks

Not deployedTimeline and evidence
  1. Opened
    Sep 6, 2026, 1:49 PM
  2. Sep 6, 2026, 4:07 PM
  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.

Summary

strategist creates status: 'recommended' tasks (soft proposals); next-steps creates status: 'todo' tasks (real, committed follow-ups). Both can independently conclude the same real-world action is needed, producing two rows for one commitment , traced concretely to a case where strategist proposed "Confirm MSA signed status with Octavio" as a recommendation, and 6 days later next-steps independently created a todo saying "Confirm MSA is signed...", with no cross-check ever catching it.

Promotion direction (explicit, one-way): recommendations promote UP into todos , never the reverse. A recommendation must never demote real, committed work, and seeing an existing recommendation must only change how a genuinely-owed action is satisfied (promote vs. create fresh) , never a reason to skip creating it.

  • Widened createTaskTool's duplicate-awareness gate to also see open recommended rows and to run when creating a recommendation, not just a todo (it used to skip entirely for status === 'recommended').
  • Recommendation creation, blocked by an open todo โ†’ returned the same recoverable, bounded-to-one-extra-turn way the todo-vs-todo case already uses.
  • Todo creation, covered by exactly one open recommendation โ†’ that recommendation is promoted in place (status flip, same row id) instead of inserting a duplicate. Zero or two-or-more open recommendations is deliberately left ambiguous and falls through to a fresh insert.
  • Extracted the promotion mutation out of the tRPC acceptRecommendation route into a shared promoteRecommendationToTodo() in services/user-tasks/tasks.ts, called by both the user's own Accept button and the tool's automatic promotion, so the two paths can never drift apart.
  • thermo-review finding, fixed before this PR: the promotion success path was missing the same postconditions a fresh insert gets , the runtime-context taskId handoff (so a chained tool in the same turn, e.g. save-draft, attaches to the right task) and the interactive taskCreated stream event. Both are now applied on promotion too, with a dedicated test.

What was already in progress vs. finished here

A prior agent in this worktree had already built most of this (the gate widening, the promote path, the extracted promoteRecommendationToTodo) but stalled mid-edit. Picking it up: found the promotedRecommendation output field was set at the call site but never added to CreateTaskOutputSchema (added it); found the promotion success path skipped the taskId handoff + stream event that a fresh insert gets (added both, per thermo-review); updated createTaskTool.recommendation-cap.test.ts's fixtures for the gate's extra openTodos query, which now also runs ahead of the ยง3.05 cap check when creating a recommendation; added createTaskTool.recommendation-promotion.test.ts covering the promote path, the recommendation-vs-todo block, and both guardrail cases (an ambiguous recommendation stack, and a promotion that races and fails) never silently dropping owed work.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm exec vitest run src/mastra/tools/task/__tests__/ , 218 passed, 2 skipped (unrelated)
  • pnpm exec vitest run on the touched trpc-route / db-sort-order test files , all pass
  • pnpm dlx oxlint@1.78.0 --deny-warnings , clean
  • pnpm deps:check , no dependency violations (services layer stays clean of Mastra imports)
  • thermo-review performed against git diff origin/staging , one ๐ŸŸก found (missing taskId handoff + stream event on promotion) and fixed; a couple of ๐ŸŸข nits noted as deliberate/out-of-scope (promotion doesn't rewrite taskActionData/threadId/notes, matching acceptRecommendation's existing narrow semantics; promoteRecommendationToTodo opens its own DB connection rather than reusing the caller's)

๐Ÿค– Generated with Claude Code

https://claude.ai/code/session_01E3AhwqA

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/tools/task/__tests__/createTaskTool.recommendation-cap.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/createTaskTool.recommendation-promotion.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/taskTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/createTaskTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/recommendation-promotion-gate.tsno production surface mapped
  • apps/server/src/mastra/tools/task/taskTool.tsno production surface mapped
  • apps/server/src/services/user-tasks/__tests__/promote-recommendation-to-todo.test.tsno production surface mapped
  • apps/server/src/services/user-tasks/tasks.tsno production surface mapped
  • apps/server/src/trpc/routes/user-tasks.tsno production surface mapped