fix(chat-workflow): persist the assistant message after a successful run

Merged
SW
5mo ago
+506-11

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 in app/workflows/chat-post-finish.ts. Fire-and-forget upsert + updated_at touch on fresh inserts; idempotent on workflow replay; swallows its own errors.
  • runAgentStep now wires an onFinish callback into toUIMessageStream to capture the assembled assistant message, and returns it as part of the new RunAgentStepResult shape ({finishReason, responseMessage}).
  • runAgentWorkflow calls persistAssistantMessage(chatId, responseMessage) after a successful runAgentStep (in the try block, before the existing clearChatActiveStream + closeChatStream finally). On throw, no message is persisted (nothing was generated); cleanup still runs.

Test plan

  • 6 new tests in persistAssistantMessage.test.ts covering insert + touch, duplicate skip, wrong-role guard, DB-error swallow, exception swallow.
  • 3 new tests in runAgentStep.test.ts covering onFinish wiring, captured-responseMessage return, undefined-when-no-fire.
  • 3 new tests in runAgentWorkflow.test.ts covering 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}/messages if 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.ts as a "use step": idempotent upsert; updates updated_at and last_assistant_message_at on fresh inserts; never throws.
    • runAgentStep captures the assembled responseMessage via onFinish and returns it; runAgentWorkflow persists it before cleanup.
  • Refactors

    • Moved id generation to the workflow body via "use step" generateAssistantMessageId; runAgentStep accepts assistantMessageId and forwards it to toUIMessageStream.generateMessageId.
    • runAgentWorkflow reuses 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.

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.

Review Change Stack