Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(chat): retry once on empty model completion in headless chat

merged#2534CedarCopilot

CedarCopilot wants to merge 0 commits into staging from fix/headless-chat-empty-response-retry

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 20, 2026, 3:31 PM
  2. Merged
    Aug 22, 2026, 9:18 AM
  3. Live on prod
    Aug 22, 2026, 9:18 AM
  4. Observed 26 hours
    Aug 22, 2026, 9:18 AM
  5. Pipelines steady after this deploy
    Aug 22, 2026, 9:18 AM
  6. Unobserved

    Live on prod, no production signal yet

    Aug 23, 2026, 11:20 AM

Behaviors Libra is checking

Slack, iMessage, and SMS headless chat retry the agent generation once when the first final completion is empty or whitespace, reducing user-visible empty responses from transient model failures.Inconclusivelow confidence

OTEL fallback found no prod spans matching runChatAgent / runImessageAgent / runSmsAgent since deploy.

prod, checked Aug 23, 2026, 11:20 AM

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

Problem

Users on headless chat surfaces intermittently got "I did not generate a response." (e.g. SimCare's Sam DMing the Slack bot , 6Γ— over ~2 weeks).

Root cause (confirmed end-to-end)

Traced the reported failure through Postgres β†’ Axiom β†’ CloudWatch β†’ the Understudy gateway logs:

  • The agent ran its tools fine, then the final model turn came back with empty text (stop, content: "").
  • Understudy confirmed: our thinking:disabled was forwarded unchanged, the raw upstream Anthropic response was genuinely empty, and upstream latency was ~44s on a clean HTTP 200.

So it's not the maxSteps ceiling and not extended thinking (both ruled out , thinking is disabled on this path and was verified off). It's a rare, transient empty completion from claude-sonnet-5 after a tool-use turn on a large context.

Fix

On an empty final answer, retry the turn once before falling back , on the Slack, iMessage, and SMS entrypoints. The empty completion is transient, so a single retry almost always recovers a real answer. Emits a warn log so the empty-completion rate is queryable in prod.

  • The Slack approval-apply path (operation-registry) is intentionally left without a retry , it's a write path with a benign fallback, so a blind retry could double-apply.

Caveat

The retry re-runs the tool loop, so a turn that performed a write before returning empty could double it. The observed case was read-only and the event is rare, so this ships the simple version; write-detection can be added later if the warn logs show it firing on write-heavy turns.

Testing

  • pnpm --filter @zero/server run types , clean (run on the identical change in-place).
  • Verified against the live Understudy gateway that thinking:disabled is honored and the empty completion is upstream.

πŸ€– Generated with Claude Code

Greptile Summary

The PR retries Slack, iMessage, and SMS headless-chat generation once when the model returns blank final text.

  • Reuses the original prompt, request context, thread, and skill-tool set for the retry.
  • Emits a warning whenever the retry is activated.
  • Currently replaces the first generation result, which loses its completed tool-call events during conversation persistence.

Confidence Score: 4/5

The retry should not merge unchanged because successful tool activity from an empty first generation can be lost from persisted conversation history.

All three retry branches overwrite the first generation result before persistence, so any tool events produced by that run are absent from the stored history even though their side effects may already have completed.

Files Needing Attention: apps/server/src/mastra/routeHandlers/slack/slack-bot-chat.ts, apps/server/src/mastra/routeHandlers/imessage/imessage-bot-chat.ts, apps/server/src/mastra/routeHandlers/sms/sms-bot-chat.ts

Important Files Changed

FilenameOverview
apps/server/src/mastra/routeHandlers/slack/slack-bot-chat.tsAdds one retry for blank agent output, but replacing result omits first-attempt tool events from persisted Slack history.
apps/server/src/mastra/routeHandlers/imessage/imessage-bot-chat.tsAdds the same empty-completion retry and first-result replacement to iMessage.
apps/server/src/mastra/routeHandlers/sms/sms-bot-chat.tsAdds the same empty-completion retry and first-result replacement to SMS.

Sequence Diagram

rendering diagram…
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/routeHandlers/imessage/imessage-bot-chat.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/shared/empty-completion-retry.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/slack/slack-bot-chat.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/sms/sms-bot-chat.tsno production surface mapped