CedarCopilot wants to merge 1 commit into staging from fix/chat-failed-message-retry
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.
Cedar's in-app AI chat (apps/mail) could fail a turn with zero visible feedback:
agentConnectionSlice.ts's stream handler had a
case 'error': that only did console.error(...) and break , no rethrow. The
existing generic error-bubble fallback in the outer catch block never got a
chance to fire, because the provider's streamLLM.completion promise resolves
normally after emitting an error event (it only calls the handler, then returns
without throwing , see providers/mastra.ts's internal try/catch). So a stream
error left the user's message sitting there with no reply and no error state.error event either , nothing ever settles the turn as failed.chat-failed message type + FailedMessageRenderer , a new bubble (ShimmerText
in its error state, matching the existing "Agent Interrupted" bubble's styling)
with a Retry button that resends the original message (same text, same
thread) through the normal sendMessage() pathway , using the same
overridePrompt replay contract messageQueueSlice already uses for queued sends.surfaceFailedChatMessage , a shared helper (agentConnectionSlice.ts) called
from every place a turn can die silently: the stream error case, a genuine
exception in sendMessage (replacing the old static "An error occurred..." text
bubble that had no retry), and a resumed stream's own error case , instead of
duplicating (or, for the stream-error path, omitting) the failure UI.sendMessage hasn't heard any stream event within SEND_ACK_TIMEOUT_MS
of sending, it aborts the stream and surfaces the same failed bubble. This is a
one-shot "did anything come back at all" timer, not a per-chunk staleness check ,
it clears on the very first event (including an early tool-call notification), so
a long multi-tool-call turn that's merely slow is unaffected; only total silence
from the start counts as vanished. Skipped if the user already stopped the run by
hand (no double "Interrupted" + "failed" bubble).There's no existing beacon/ack pattern in this codebase for that case (checked:
providers/mastra.ts, agentUtils.ts, no AbortSignal.timeout or health-check
usage anywhere in agentConnection/). The simplest reliable mechanism given the
existing StreamHandler/StreamResponse shape is a client-side "first event"
timer: schedule it right after streamLLM() returns, clear it in the wrapped
handler on the first event of any type, and treat a fully-silent timeout exactly
like a stream error (abort + same failed bubble). 45s is a judgment call , generous
enough for a cold start, but open to tuning if telemetry says otherwise.
agentConnectionSlice.failedMessage.test.tsx , stream error → one failed bubble
(and exactly one even if the stream errors twice); an exception thrown before
streaming starts → failed bubble; ack-timeout fires + aborts when nothing ever
arrives; ack-timeout stays quiet (no abort, no bubble) when the user already
cancelled the run.FailedMessageRenderer.test.tsx , clicking Retry resends the original
threadId/overridePrompt through sendMessage().pnpm --filter @zero/mail run types , same pre-existing error set as staging
(paraglide/react-router codegen +types/@/paraglide/* modules not generated in
this checkout); no new errors in any touched file.pnpm deps:check , no dependency violations (1833 modules, 9699 dependencies).pnpm --filter @zero/mail exec jest modules/cedar-os , 31 suites / 443 tests pass,
including the 6 new ones.Libra has not measured any production surfaces for this change yet.