CedarCopilot wants to merge 1 commit into staging from fix/sherlock-slack-chat-malformed-json-20260920-154713
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
Isabelle reported an intermittent malformed Slack message where the literal JSON payload , including a broken "buttons":[...] array (a dropped "label" key) , showed up as the visible message text instead of rendering as prose + real buttons.
runChatAgent (slack-bot-chat.ts) asks the chat agent to hand-write a JSON blob inside <final_answer> tags, then does a bare JSON.parse(rawText). When that JSON is invalid , e.g. the agent quotes an exact customer phrase with plain double quotes (raised the "tasks don't auto-complete..." issue), which breaks the JSON string , the catch block falls through to using the entire raw text, including the literal "buttons":[...] array, as the visible Slack message. No repair, no retry, no safe fallback.
Confirmed live against today's actual chat history for : pulled the two real persisted responses to her "stale tasks" question, ran them through the literal production JSON.parse call, and both threw at the exact unescaped-quote position , reproducing the bug class exactly.
Replace the manual JSON.parse with a tool-free, schema-constrained structuring pass (chatResponseStructuringAgent, new export in chat-agent.ts) that reshapes the main agent's draft text into SlackResponseSchema via Mastra's structuredOutput. Schema-constrained decoding can't emit invalid JSON, so it repairs the common failure modes (unescaped inner quotes, a double-encoded text field, a truncated field) instead of failing outright, and errorStrategy: 'fallback' guarantees a clean generic message for whatever still doesn't fit , never a raw-text leak.
Deliberately a separate call with no tools attached (not bolted onto the same .generate() call as the tool-calling agent) , combining tools and structuredOutput in one call is unsupported by some model providers (Mastra's docs flag this for Gemini 2.5 + function calling). The main tool-calling loop is unchanged.
Also loosens ActionButtonSchema.style from .optional().default('default') to .optional() , the .default() made the schema's own inferred type require style, but the real structuredOutput result type doesn't guarantee it's present, and the only consumer (buildBlockKitBlocks) already treats a missing style identically to 'default'.
pnpm --filter @zero/server run types , clean.vitest run on the 4 affected test files (35 tests) , all pass, including an updated mock in surface-context-parity.test.ts for the new structuring call.chatResponseStructuringAgent, exercising:
"label" key) , recovered with a valid, functional button (falls back to a generic label only for the one button whose original label was genuinely unrecoverable, rather than leaking JSON or crashing).imessage-bot-chat.ts and sms-bot-chat.ts have the identical hand-rolled JSON.parse pattern. Same bug class, same fix would apply, but out of scope for this report.
š¤ Generated with Claude Code
Libra has not measured any production surfaces for this change yet.