Skip to content

Migration Guide

Upgrading to 0.6.0

Compaction now archives events instead of deleting them, so the full history stays searchable through Recall Storage (issue #21). This introduces four breaking changes.

Breaking: core module artifact renamed from spring-ai-session-management to spring-ai-session

The groupId (org.springaicommunity) and Java package (org.springframework.ai.session) are unchanged — only the Maven artifact/module name moved:

<!-- Before (0.5.x) -->
<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>spring-ai-session-management</artifactId>
</dependency>

<!-- After (0.6.0) -->
<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>spring-ai-session</artifactId>
</dependency>

No other module (spring-ai-session-jdbc, spring-ai-autoconfigure-session, spring-ai-autoconfigure-session-jdbc, spring-ai-starter-session-jdbc, spring-ai-session-bom) changed name. If you depend on the JDBC starter or the BOM rather than the core artifact directly, no change is needed.

Breaking: SessionRepository.replaceEvents(...) replaced by compactEvents(...)

Both replaceEvents overloads are removed. Custom SessionRepository implementations must implement the new method instead:

// Before (0.5.x)
boolean replaceEvents(String sessionId, List<SessionEvent> events, long expectedVersion);

// After (0.6.0): archive the removed events, swap in the new active window
boolean compactEvents(String sessionId, List<SessionEvent> archivedEvents,
        List<SessionEvent> retainedEvents, long expectedVersion);

The implementation must mark archivedEvents as archived (retained, not deleted), set the active event set to retainedEvents, and preserve any previously-archived events.

Breaking: getEvents(sessionId) / getMessages(sessionId) now include archived events

EventFilter.all() (the default for the no-arg accessors) returns the full log, archived events included. To get the active context window — what you pass to the model — use the new EventFilter.active():

// Active context window only (archived events excluded)
sessionService.getEvents(sessionId, EventFilter.active());

// Recall Storage: full history, archived included (unchanged)
sessionService.getEvents(sessionId, EventFilter.keywordSearch("topic"));

SessionMemoryAdvisor already forces active() when building the prompt, so no change is needed there.

Breaking: JDBC schema adds seq and archived columns

AI_SESSION_EVENT gains an archived flag and a monotonic seq ordering column (events are now ordered by seq, not timestamp). Recreate the table or apply, e.g. for H2:

ALTER TABLE AI_SESSION_EVENT ADD COLUMN archived BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE AI_SESSION_EVENT ADD COLUMN seq BIGINT GENERATED BY DEFAULT AS IDENTITY;

See the bundled schema-{h2,postgresql,mysql}.sql for the full DDL per dialect.

Upgrading to 0.3.0

Breaking: SessionMemoryAdvisor.Builder.defaultSessionId() removed

What changed

defaultSessionId(String) has been removed from SessionMemoryAdvisor.Builder. The advisor no longer falls back to a shared session — SESSION_ID_CONTEXT_KEY is now required on every request. Omitting it throws IllegalStateException.

Why

A single default session ID was shared across all requests to the same advisor instance, silently merging conversation history from different users or threads. This is a correctness and security issue in any multi-user deployment.

How to migrate

Before (0.2.x):

SessionMemoryAdvisor advisor = SessionMemoryAdvisor.builder(sessionService)
    .defaultSessionId("my-session-id")   // ← removed
    .build();

// Session ID came from the advisor default — no per-request param needed
client.prompt().user("Hello").call().content();

After (0.3.0):

SessionMemoryAdvisor advisor = SessionMemoryAdvisor.builder(sessionService)
    .build();

// Session ID must be passed on every request
client.prompt()
    .user("Hello")
    .advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
    .call()
    .content();

In a typical web application, resolve the session ID from the authenticated principal or HTTP session in the controller, then pass it per call:

@PostMapping("/chat")
String chat(@AuthenticationPrincipal UserDetails user, @RequestBody String message) {
    String sessionId = resolveSessionId(user.getUsername());
    return chatClient.prompt()
        .user(message)
        .advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
        .call()
        .content();
}