agentscope-ai / agentscope-ai/agentscope-java
[Bug]:AgentState is persisted under a corrupted (userId, sessionId) slot when sessionId contains '/'
- Vorherrschende Sprache
- Java
- Sterne
- 5.6k
- Forks
- 1.3k
- Ø Merge
- 4 T. 12 Std.
- Gemergte PRs (30 T.)
- 77
Beschreibung
## 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.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.