{"record":{"id":"ab08e916e9abb00f","repo":"spring-projects/spring-ai","slug":"jdbcchatmemoryrepository-does-not-support-tool-cal","errorCode":null,"errorMessage":"JdbcChatMemoryRepository does not support tool call messages. Some messages were filtered out for conversation: + conversationId","messagePattern":"JdbcChatMemoryRepository does not support tool call messages\\. Some messages were filtered out for conversation: \\+ conversationId","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"memory-repositories/spring-ai-model-chat-memory-repository-jdbc/src/main/java/org/springframework/ai/chat/memory/repository/jdbc/JdbcChatMemoryRepository.java","lineNumber":117,"sourceCode":"\t\tAssert.hasText(conversationId, \"conversationId cannot be null or empty\");\n\t\treturn this.jdbcTemplate.query(this.dialect.getSelectMessagesSql(), new MessageRowMapper(), conversationId)\n\t\t\t.stream()\n\t\t\t.filter(Objects::nonNull)\n\t\t\t.toList();\n\t}\n\n\t@Override\n\tpublic void saveAll(String conversationId, List<Message> messages) {\n\t\tAssert.hasText(conversationId, \"conversationId cannot be null or empty\");\n\t\tAssert.notNull(messages, \"messages cannot be null\");\n\t\tAssert.noNullElements(messages, \"messages cannot contain null elements\");\n\n\t\tList<Message> persistableMessages = messages.stream()\n\t\t\t.filter(m -> !(m instanceof ToolResponseMessage)\n\t\t\t\t\t&& !(m instanceof AssistantMessage am && am.hasToolCalls()))\n\t\t\t.toList();\n\t\tif (logger.isWarnEnabled() && persistableMessages.size() < messages.size()) {\n\t\t\tlogger.warn(\n\t\t\t\t\t\"JdbcChatMemoryRepository does not support tool call messages. Some messages were filtered out for conversation: \"\n\t\t\t\t\t\t\t+ conversationId);\n\t\t}\n\n\t\tthis.transactionTemplate.executeWithoutResult(status -> {\n\t\t\tdeleteByConversationId(conversationId);\n\t\t\tthis.jdbcTemplate.batchUpdate(this.dialect.getInsertMessageSql(),\n\t\t\t\t\tnew AddBatchPreparedStatement(conversationId, persistableMessages));\n\t\t});\n\t}\n\n\t@Override\n\tpublic void deleteByConversationId(String conversationId) {\n\t\tAssert.hasText(conversationId, \"conversationId cannot be null or empty\");\n\t\tthis.jdbcTemplate.update(this.dialect.getDeleteMessagesSql(), conversationId);\n\t}\n\n\tpublic static Builder builder() {","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/memory-repositories/spring-ai-model-chat-memory-repository-jdbc/src/main/java/org/springframework/ai/chat/memory/repository/jdbc/JdbcChatMemoryRepository.java#L99-L135","documentation":"JdbcChatMemoryRepository.saveAll() filters out ToolResponseMessage and tool-calling AssistantMessage rows because the JDBC schema has no representation for them, logging this warning when messages were dropped. Tool-call context is not persisted, so reloaded conversations lose tool history.","triggerScenarios":"Calling saveAll (directly or via addAndGet) with a message list containing ToolResponseMessage or AssistantMessage with tool calls for the given conversationId; saveAll then deletes and rewrites the conversation rows without them.","commonSituations":"Tool-calling agents (OpenAI function calling, etc.) using JDBC-backed ChatMemory; switching from an in-memory repository (which kept tool messages) to JDBC and noticing lost tool history; long agent conversations that exceed context after reload.","solutions":["Pre-filter tool messages in your own code before calling saveAll so the drop is intentional and warning-free","If tool history is required, use a store/abstraction that persists tool-call messages (e.g. keep in-memory repository, or upgrade to a version adding tool support)","Store tool interactions in a separate application table and merge them back when loading conversation history","Check for a newer spring-ai version — tool-call memory support has been expanding across repositories"],"exampleFix":"// before\nmemory.add(conversationId, toolResponseMessage); // silently filtered, warning logged\n\n// after\nif (msg instanceof ToolResponseMessage || (msg instanceof AssistantMessage am && am.hasToolCalls())) {\n    toolHistoryStore.save(conversationId, msg); // your own persistence\n} else {\n    memory.add(conversationId, msg);\n}","handlingStrategy":"validation","validationCode":"boolean persistable(Message m) {\n    return !(m instanceof ToolResponseMessage)\n        && !(m instanceof AssistantMessage am && am.hasToolCalls());\n}","typeGuard":"static boolean dropsToolHistory(Message m) {\n    return m instanceof ToolResponseMessage\n        || (m instanceof AssistantMessage am && am.hasToolCalls());\n}","tryCatchPattern":null,"preventionTips":["Pre-filter tool messages before saveAll/addAndGet with JDBC memory","Test conversation round-trips (save + reload) in CI with tool-calling flows","If tool history matters, choose a store that persists it or persist separately","Watch the warn log as a signal your memory backend loses tool context"],"tags":["jdbc","chat-memory","tool-calls","message-filtering"],"backgroundTag":"unsupported-operation","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}