fix(chat-workflow): persist the assistant message after a successful run
Summary
Closes the silent-data-loss gap that the open-agents → recoup-api chat-workflow cutover introduced. Pre-PR, assistant replies streamed to the client over SSE but were never written to chat_messages — so any page refresh after a successful exchange wiped the reply.
This is Tier 1, item 1 from #605 (highest-impact remaining gap after #604 / #607).
Changes
- New
lib/chat/persistAssistantMessage.ts"use step" — mirrors open-agents' helper of the same name inapp/workflows/chat-post-finish.ts. Fire-and-forget upsert +updated_attouch on fresh inserts; idempotent on workflow replay; swallows its own errors. runAgentStepnow wires anonFinishcallback intotoUIMessageStreamto capture the assembled assistant message, and returns it as part of the newRunAgentStepResultshape ({finishReason, responseMessage}).runAgentWorkflowcallspersistAssistantMessage(chatId, responseMessage)after a successfulrunAgentStep(in the try block, before the existingclearChatActiveStream+closeChatStreamfinally). On throw, no message is persisted (nothing was generated); cleanup still runs.
Test plan
- 6 new tests in
persistAssistantMessage.test.tscovering insert + touch, duplicate skip, wrong-role guard, DB-error swallow, exception swallow. - 3 new tests in
runAgentStep.test.tscovering onFinish wiring, captured-responseMessage return, undefined-when-no-fire. - 3 new tests in
runAgentWorkflow.test.tscovering persist on success, no-persist when responseMessage undefined, no-persist on throw while cleanup still runs. - Full suite: 3159 → 3171 passing.
- Lint clean.
- Manual probe on PR preview — send a message, confirm via direct DB query (or
GET /api/sessions/{sessionId}/chats/{chatId}/messagesif that exists) that the assistant message row is present.
Tracked in #605
This PR closes the first Tier-1 item. Remaining: stop monitor, sandbox state persist, tool results persist, chat title generation, then Tier 2 multi-step verification.
🤖 Generated with Claude Code
Summary by cubic
Persists the final assistant message to chat_messages, uses a stable assistant message id so the upsert lands, and bumps last_assistant_message_at to keep unread badges accurate. Addresses Tier 1, item 1 in #605.
-
Bug Fixes
- Added
lib/chat/persistAssistantMessage.tsas a "use step": idempotent upsert; updatesupdated_atandlast_assistant_message_aton fresh inserts; never throws. runAgentStepcaptures the assembledresponseMessageviaonFinishand returns it;runAgentWorkflowpersists it before cleanup.
- Added
-
Refactors
- Moved id generation to the workflow body via "use step"
generateAssistantMessageId;runAgentStepacceptsassistantMessageIdand forwards it totoUIMessageStream.generateMessageId. runAgentWorkflowreuses the latest assistant id when resuming a tool-call turn. Tests cover persistence, id generation/forwarding, resume path, and unread timestamp updates.- Format-only cleanup of workflow files to satisfy lint/CI.
- Moved id generation to the workflow body via "use step"
Summary by CodeRabbit
- Bug Fixes
- Assistant responses are now reliably saved to chat history after streaming completes, preserving full conversation records.
- Final assistant messages are de-duplicated and correctly associated with a stable message ID so streamed content remains consistent across retries.
- Chat metadata (timestamps) is updated when a new assistant message is persisted.