agentscope-ai / agentscope-ai/agentscope-java

[Bug]:AgentState is persisted under a corrupted (userId, sessionId) slot when sessionId contains '/'

Abierto
#2,475 1 comentario 0 reacciones 0 asignados Ver en GitHub
area/build area/core/agent area/core/memory bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

## Bug Description

When `RuntimeContext.sessionId` contains a `/` character, `ReActAgent` persists the `AgentState` under a **corrupted `(userId, sessionId)` pair** at the end of each call. The state is written to the `AgentStateStore` with the wrong keys, so any subsequent read with the original `(userId, sessionId)` — e.g. `getAgentState(userId, sessionId)` for conversation-history replay — always returns an empty/fresh state.

This is not an exotic input: DingTalk `openConversationId` values (commonly used as sessionIds in ChatOps scenarios) are standard Base64 and legitimately contain `/`, e.g. `cidLcMOzfRs62haqnq1NO8SxeYdmc/lO8bWyidOisoKAzM=`.

## Affected Version

- agentscope-java `2.0.0` (still present on current `main`)

## Root Cause

`ReActAgent` builds an internal slot key by concatenating the identity with `/`:

```java
// ReActAgent#slotKey
private static String slotKey(String userId, String sessionId) {
return (userId == null || userId.isBlank() ? "__anon__" : userId) + "/" + sessionId;
}
```

At end-of-call persistence (`saveStateToSession`), the pair is re-derived by **splitting the concatenated key at the last `/`**:

```java
// ReActAgent$SlotRef
static SlotRef parse(String slotKey) {
int slash = slotKey.lastIndexOf('/'); // <-- breaks when sessionId contains '/'
String u = slotKey.substring(0, slash);
String s = slotKey.substring(slash + 1);
return new SlotRef("__anon__".equals(u) ? null : u, s);
}
```

For `userId = "u1"`, `sessionId = "cidLcMOzfRs62haqnq1NO8SxeYdmc/lO8bWyidOisoKAzM="`:

- slot key: `u1/cidLcMOzfRs62haqnq1NO8SxeYdmc/lO8bWyidOisoKAzM=`
- parsed back as: `userId = "u1/cidLcMOzfRs62haqnq1NO8SxeYdmc"`, `sessionId = "lO8bWyidOisoKAzM="`

`stateStore.save(...)` is then called with these corrupted keys. Note that `saveAgentState(userId, sessionId)` (the public admin API) passes the identity through directly and is NOT affected — so the same session can end up with two diverging rows in the store (one correct, one corrupted), depending on which code path performed the last write.

## Reproduction

```java
InMemoryAgentStateStore store = new InMemoryAgentStateStore();
ReActAgent agent = ReActAgent.builder()
.name("asst").sysPrompt("hi").model(anyModel).stateStore(store).build();

String sessionId = "cidLcMOzfRs62haqnq1NO8SxeYdmc/lO8bWyidOisoKAzM=";
RuntimeContext ctx = RuntimeContext.builder().userId("u1").sessionId(sessionId).build();
agent.call(List.of(userMsg("hello")), ctx).block();

// EXPECTED: state stored under ("u1", sessionId)
store.get("u1", sessionId, "agent_state", AgentState.class); // -> empty (BUG)

// ACTUAL: state stored under a '/'-split pair
store.listSessionIds("u1/cidLcMOzfRs62haqnq1NO8SxeYdmc"); // -> ["lO8bWyidOisoKAzM="]
```

## Impact

- Conversation history replay via `getAgentState(userId, sessionId)` silently returns empty for any session whose id contains `/` — multi-turn context is effectively lost across calls that rely on the persisted state (each call start reloads from the store using the *correct* keys, so it never sees what the previous call saved).
- State-store rows accumulate under corrupted `user_id` values (e.g. `u1/cidLcMOzfRs62haqnq1NO8SxeYdmc`), which also breaks per-user queries such as `listSessionIds(userId)`.

## Suggested Fix

Do not round-trip the identity through the concatenated slot key. Carry the original `(userId, sessionId)` on the per-call scope (`CallExecution`, resolved once in `activateSlotForContext`) and use them directly in `saveStateToSession`; remove `SlotRef.parse`. The concatenated `slotKey` can remain as an opaque cache/serialization key, but it must never be split back.

A regression test suggestion: run a call with a sessionId containing `/` and assert the state is retrievable from the store under the untouched pair, and that no sibling slot appears under a polluted userId.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.