CedarCopilot wants to merge 1 commit into staging from fix/family-surface-task-recommendations
Live on prod, no production signal yet
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.
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:
| field | state on task.list | consequence |
|---|---|---|
taskType | declared + forwarded | Removed from listTasksTool (split into groups + outputKinds). Zod strips an unknown key, so "just show me follow-ups" returned everything, no error anywhere. |
groups, outputKinds | never declared | No lane filter and no artifact filter on the entire family surface. |
includeRecommended | never declared | recommended 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.
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 validateToolInput → convertUndefinedToNull 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.
taskType removal callRemoved, and it is not a breaking change.
Libra has not measured any production surfaces for this change yet.