agentscope-ai / agentscope-ai/agentscope-java

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

Aberta
#2,475 1 comentário 0 reações 0 responsáveis Ver no GitHub
area/build area/core/agent area/core/memory bug
Linguagem predominante
Java
Estrelas
5.6k
Forks
1.3k
Merge médio
4d 12h
PRs com merge (30d)
77

Descrição

## 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.

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.