Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(task-tool): make recommendations reachable on the family surface

merged#2663CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/family-surface-task-recommendations

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 2, 2026, 11:17 AM
  2. Sep 2, 2026, 11:27 AM
  3. Merged
    Sep 2, 2026, 11:42 AM
  4. Live on prod
    Sep 2, 2026, 11:42 AM
  5. Observed 0 hours
    Sep 2, 2026, 11:42 AM
  6. Unobserved

    Live on prod, no production signal yet

    Sep 2, 2026, 11:42 AM
  7. Pipelines steady after this deploy
    Sep 2, 2026, 11:42 AM

Behaviors Libra is checking

Creating a task through the family surface persists the supplied tags instead of silently discarding them.Not checked
prod
The family list action forwards the delegate's complete canonical filter set, including scope, targetUserId, activeAopNames, excludeStatuses, groups, outputKinds, and includeRecommended, so newly supported list filters do not get silently dropped.Not checked
prod
The family task surface can filter listed tasks by lane using the canonical groups parameter instead of the obsolete taskType parameter.Not checked
prod
Listing tasks with omitted optional filters no longer sends undefined values that Mastra converts to null and rejects during delegate validation.Not checked
prod
The family task surface can filter listed tasks by the artifact they produce using outputKinds, such as email.Not checked
prod
Task recommendations are hidden by default on the family surface and become reachable, marked [RECOMMENDATION], when includeRecommended is explicitly true.Not checked
prod

Libra has verdicts on 0 of 7 tracked behaviors on prod; 7 are still being checked. Libra checks hourly for 3 days after each deploy.

What was broken

user_tasks rows with status = 'recommended' are agent PROPOSALS the user has not accepted. There are two agent tool surfaces, and they disagreed about whether those exist.

Granular surface (getAllSkillTools() , the current default for the in-app chat agent, and what every subagent runs on): the agent gets list-tasks directly, which has an includeRecommended opt-in. Recommendations are reachable. Fine.

Family surface (buildMasterFamilyToolset() , the cedar-master-agent-families flag, "Cedar (fast)", the external MCP server's FAMILY_TOOLS, and the SDK harness): the agent gets the consolidated task tool, whose list action re-typed listTasksTool's params by hand in its own ListParamsSchema. That copy had drifted three ways, all silent, all failing open rather than erroring:

fieldstate on task.listconsequence
taskTypedeclared + forwardedRemoved from listTasksTool (split into groups + outputKinds). Zod strips an unknown key, so "just show me follow-ups" returned everything, no error anywhere.
groups, outputKindsnever declaredNo lane filter and no artifact filter on the entire family surface.
includeRecommendednever declaredrecommended tasks unconditionally invisible, with no way to ask for them.

The last one is the headline: on those surfaces an agent could not answer "what am I already proposing on this deal?", and could not prune its own standing proposals.

What changed

ListParamsSchema is now derived, not re-typed.

const ListParamsSchema = ListTasksInputSchema.extend({ conversationId: /* … */ });

and the dispatch forwards the params object whole rather than naming each field:

const { conversationId, ...listParams } = inputData.list ?? {};
await listTasksTool.execute(omitUndefined(listParams), callContext);

That closes both drift surfaces at once , the declaration and the forwarding , so a field added or renamed on listTasksTool can no longer go missing here. It also deletes more code than it adds.

I initially did this the way the issue described (hand-add the three fields, keep the per-field conditional spread, and add a 113-line reflection-based schema-parity test to catch future drift). Self-review correctly rejected that: the file's own stated justification for re-typing , "each action's params must be nestable under a same-named key, per the MCP SDK note" , is false. .extend() returns a ZodObject, which has .shape, which is the only thing normalizeObjectSchema checks; and the .shape gap only ever applied to the root schema. conversationTool.schemas.ts:101 already derives UpdateFieldsParamsSchema exactly this way, with a comment recording that its hand-retyped copy had drifted and that deriving was the fix. So this is a return to the house pattern, not a new one , and the parity test became unnecessary and was dropped.

omitUndefined() is still required and is not decoration: Mastra's validateToolInputconvertUndefinedToNull turns a present-but-undefined key into null before validating against listTasksTool's schema, whose fields are .optional() but not .nullable(). A Zod parse leaves omitted optional keys present-and-undefined, so anything forwarded wholesale has to be compacted first. This is the production error the old per-field conditional spread existed to prevent; the helper generalises it instead of discarding it. It returns Partial<T> rather than T so it needs no cast.

Tool description now tells a model reading only the family tool that list hides recommendations unless includeRecommended: true, and that they come back marked [RECOMMENDATION]. Note the existing tool-description-length.test.ts guard caught my first draft at 2079 chars against a 2048 limit , the shipped wording fits with headroom.

The taskType removal call

Removed, and it is not a breaking change.

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/.claude/skills/tasks/SKILL.mdno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/taskTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/listTasksTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/taskTool.tsno production surface mapped
  • docs/testing/phase4-manual-test-guide.mdno production surface mapped