Files
spring-ai/chat-memory/README.md
T
Claude 823f6fac5b Add chat-memory module: MessageWindowChatMemory, JDBC and Redis repositories, per-user conversation IDs
Real PostgreSQL 16 and Redis Stack 7.4; 24 tests write output/01-24. Covers the 20-message
default window with no property, the 36-character conversation_id, tool messages dropped by
JdbcChatMemoryRepository, concurrent add() on one conversation, a 1.x table under the 2.0
repository, the Redis repository silently backing off for a custom ChatMemory, and the
removal of PromptChatMemoryAdvisor.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ja4jkzrbQ4LQZBNrb5mkZE
2026-09-24 15:56:47 +00:00

68 lines
4.6 KiB
Markdown

# chat-memory
Companion code for [Chat Memory in Spring AI 2.0: JDBC, Redis and Windowed Conversations](https://ankurm.com/spring-ai-2-0-chat-memory-jdbc-redis-windowed-conversations/), part of the [Spring AI series](../README.md) on ankurm.com.
`MessageChatMemoryAdvisor` + `MessageWindowChatMemory` + a `ChatMemoryRepository` (in-memory, PostgreSQL through JDBC, or Redis Stack), with per-user conversation IDs, a token-budget memory of our own, and every trap the article describes reproduced by a test that writes its own transcript.
There is no live model anywhere: [`ScriptedChatModel`](src/test/java/com/ankurm/chatmemory/support/ScriptedChatModel.java) records the prompts it is sent, which is what a memory test needs to prove -- *which messages reached the model*. The databases are real.
## Versions
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| Spring AI | 2.0.1 (GA 2026-06-12 for 2.0.0; 2.0.1 on Maven Central 2026-08-20) |
| PostgreSQL | 16.13 |
| Redis Stack | 7.4.7 (RediSearch 2.10.20, RedisJSON) |
| Jedis (pulled in by the Redis starter) | 7.4.1 |
| Java | 25 (LTS) |
## Quickstart
```bash
scripts/services-up.sh # PostgreSQL 16 (apt), Redis Stack (docker), plain Redis (apt) -- optional
scripts/run-all.sh # runs all 24 tests and regenerates output/01 .. 24
```
Tests that need a service which is not listening are **skipped with a message**, not failed, so the in-memory tests still run with nothing installed. Run the app itself with `mvn spring-boot:run` (in-memory), `-Dspring-boot.run.profiles=jdbc` or `=redis`; it needs `OPENAI_API_KEY` for real answers.
## What's here
| File | What it shows |
|---|---|
| [`config/MemoryConfig.java`](src/main/java/com/ankurm/chatmemory/config/MemoryConfig.java) | The `ChatMemory` bean (window size is not configurable by property) and the `ChatClient` with `MessageChatMemoryAdvisor` |
| [`config/RedisMemoryConfig.java`](src/main/java/com/ankurm/chatmemory/config/RedisMemoryConfig.java) | Why the Redis repository is built by hand: the autoconfigured one silently steps aside for a custom `ChatMemory` |
| [`memory/ConversationRegistry.java`](src/main/java/com/ankurm/chatmemory/memory/ConversationRegistry.java) | Who owns which conversation; `app.ownership.enforce=false` reproduces the leak |
| [`memory/ConversationService.java`](src/main/java/com/ankurm/chatmemory/memory/ConversationService.java) | Passing `ChatMemory.CONVERSATION_ID` per request |
| [`memory/TokenBudgetChatMemory.java`](src/main/java/com/ankurm/chatmemory/memory/TokenBudgetChatMemory.java) | A `ChatMemory` that prunes by tokens instead of by message count |
| [`web/ChatController.java`](src/main/java/com/ankurm/chatmemory/web/ChatController.java) | Four endpoints; the user comes from an `X-User` header only so the tests need no login |
| [`application*.yml`](src/main/resources) | Profiles `jdbc` and `redis`; the default excludes both repository autoconfigurations |
| [`src/broken/PromptAdvisorFrom1x.java`](src/broken/PromptAdvisorFrom1x.java) | Not compiled by the build; `scripts/capture-1x-compile.sh` compiles it against 2.0.1 and 1.1.8 |
## Output files
Every file is written by the test named in the right column (or by the script), and every console block in the article is quoted from one of them.
| File | Written by |
|---|---|
| `01-window-semantics.txt` | `WindowSemanticsTest` |
| `02-advisor-conversations.txt`, `03-missing-conversation-id.txt` | `AdvisorConversationTest` |
| `04-autoconfigured-defaults.txt`, `05-no-window-property.txt` | `DefaultWindowTest` |
| `06-token-growth.txt` | `TokenGrowthTest` |
| `07-token-budget-memory.txt` | `TokenBudgetChatMemoryTest` |
| `08` .. `14` (JDBC round trip, 36-character limit, dropped tool messages, lost update, overlapping saves, 1.x table upgrade, locked fix) | `JdbcPostgresTest` |
| `15-redis-round-trip.txt`, `16-redis-lost-update.txt` | `RedisStackTest` |
| `17-redis-silent-fallback.txt` | `RedisSilentFallbackTest` |
| `18-redis-ttl-and-cap.txt` | `RedisCapsAndTtlTest` |
| `19-redis-plain-fails.txt` | `RedisPlainFailureTest` |
| `20-ownership-enforced.txt`, `21-ownership-unenforced.txt` | `OwnershipTest`, `OwnershipLeakTest` |
| `22-config-keys.txt` | `ConfigKeysTest` |
| `23-prompt-advisor-removed.txt` | `scripts/capture-1x-compile.sh` |
| `24-memory-with-tool-calling.txt` | `ToolCallingMemoryTest` |
Two consecutive `mvn test` runs produce byte-identical files. `12-jdbc-overlapping-saves.txt` deliberately prints no exact count: two `saveAll` calls that overlap produce 3 or 6 messages depending on timing, and the file says so instead of pinning one run.
## Requirements
JDK 25, Maven. PostgreSQL 16 and Redis Stack are optional (see above); Docker is only used to run Redis Stack.