Skip to content

fix(session,shutdown): MysqlSession.delete impl + GracefulShutdownHook dedup improvement#1511

Open
Xingxing-Wang wants to merge 5 commits into
agentscope-ai:mainfrom
Xingxing-Wang:fix/shutdown-and-session-delete
Open

fix(session,shutdown): MysqlSession.delete impl + GracefulShutdownHook dedup improvement#1511
Xingxing-Wang wants to merge 5 commits into
agentscope-ai:mainfrom
Xingxing-Wang:fix/shutdown-and-session-delete

Conversation

@Xingxing-Wang
Copy link
Copy Markdown

Summary

  • MysqlSession: Add delete(SessionKey, String key) implementation to fix the bug where clearing flags via Session.delete() had no effect (the method was not implemented).
  • GracefulShutdownHook: Improve deduplicateIfResuming logic — only discard input when its content matches the last USER message in memory, preventing legitimate new messages from being dropped after a shutdown-interrupted resume.

Test plan

  • Verified MysqlSession.delete correctly removes state entries from DB
  • Verified GracefulShutdownHook keeps new user input when content differs from memory
  • Verified GracefulShutdownHook still discards duplicate input when content matches

Xingxing-Wang and others added 2 commits May 28, 2026 11:08
…去重优化

- MysqlSession: 添加 delete(SessionKey, String key) 实现,修复 flag 清除无效的 bug
- GracefulShutdownHook: 改进 deduplicateIfResuming 逻辑,只在新输入与 memory 中最后一条 USER 消息内容相同时才丢弃,避免误丢合法新消息

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Xingxing-Wang Xingxing-Wang requested a review from a team May 28, 2026 03:26
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 28, 2026

CLA assistant check
All committers have signed the CLA.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

❌ Patch coverage is 94.59459% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...agentscope/core/shutdown/GracefulShutdownHook.java 90.47% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

PreCallEvent.getMemory() returns null when the agent is not a ReActAgent
(e.g., in tests with mock agents). Added null guard before accessing
memory.getMessages().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Xingxing-Wang Xingxing-Wang force-pushed the fix/shutdown-and-session-delete branch from ffef06f to 99e4c39 Compare May 28, 2026 05:50
Xingxing-Wang and others added 2 commits May 28, 2026 14:11
…ook dedup logic

- GracefulShutdownHook: test input-differs-from-memory (keep), input-matches (discard),
  empty-input (early return), no-USER-in-memory (keep)
- MysqlSession: test delete(SessionKey, String key) success, commit behavior, and failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/core/memory Memory, session, state area/ext/memory Memory/session extension implementations labels May 28, 2026
Copy link
Copy Markdown
Collaborator

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review

This PR fixes two real bugs: (1) MysqlSession.delete(SessionKey, String) was unimplemented (silently a no-op via the interface default), so flag-clearing via the per-key delete had no effect on MySQL-backed sessions; (2) GracefulShutdownHook.deduplicateIfResuming unconditionally discarded any new input after a shutdown-interrupted session, which could drop legitimate new prompts. The new dedup logic correctly compares the incoming text against the last USER message in memory and only discards when they match. Test coverage is reasonable and the MySQL implementation follows the existing transactional write pattern. Two concerns worth addressing before merge: (a) the dedup compares only getTextContent() of the first input message, so non-text content (image-only / tool messages) collapses to an empty string and may false-match; (b) when event.getMemory() is null the new code still drops input — that preserves the legacy behavior validated by the existing preCallDeduplication test, but contradicts this PR's stated intent for memoryless agents. Also, MysqlSession.delete(sessionKey, key) does not clean up the companion key + ":_hash" row used by list-state change detection, leaving an orphan row (correctness still works on the next save via full-rewrite fallback, but it is inconsistent).

}

// Extract the text of the first incoming message
String newInputText = newInput.get(0).getTextContent();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Msg.getTextContent() returns the concatenation of TextBlock content joined by \n, and returns an empty string when the message has no text blocks (image-only, audio-only, or pure tool-result messages). Comparing only the text payload means: (1) two distinct image-only user messages both yield "" and will be falsely deduplicated; (2) any message that carries information solely in non-text blocks cannot be distinguished. Consider comparing by Msg.getId() when available, or by a hash over the full content block list, so the dedup check stays correct for multimodal inputs.

String newInputId = newInput.get(0).getId();
// ...later, when scanning memory for the last USER msg:
String lastUserId = msg.getId();
if (newInputId != null && newInputId.equals(lastUserId)) { /* discard */ }

// Find the last USER message in memory
Memory memory = event.getMemory();
if (memory == null) {
event.setInputMessages(List.of());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] When event.getMemory() is null (non-ReActAgent, or a ReActAgent configured without memory), this branch still calls event.setInputMessages(List.of()) and returns. That is exactly the legacy behavior this PR set out to fix: an agent without memory has no saved context to "resume" from, so silently dropping the input means the next invocation produces nothing. The branch is currently needed only because the existing preCallDeduplication test uses a non-ReActAgent (TestableAgent) and asserts an empty result. Suggest flipping the policy: when memory is unavailable, keep the new input (and update / replace the legacy test) — that aligns with the PR description ("prevent legitimate new messages from being dropped").

List<Msg> newInput = event.getInputMessages();

// No new input, nothing to deduplicate
if (newInput == null || newInput.isEmpty()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] newInput == null is unreachable: PreCallEvent's constructor wraps the list with Objects.requireNonNull(...) and setInputMessages also rejects null, so getInputMessages() cannot return null. The isEmpty() short-circuit is fine, but you can drop the null check (or replace with an assert).

}

// Extract the text of the first incoming message
String newInputText = newInput.get(0).getTextContent();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Only newInput.get(0) is inspected. If a caller ever passes multiple messages in a single resume call (e.g., a user message followed by an attachment placeholder), only the first is compared against memory; on a match the entire list is dropped — silently losing the rest. The shutdown-resume case is usually single-message, but a defensive comment, or scanning all input messages, would make the contract explicit.

validateStateKey(key);

String deleteSql =
"DELETE FROM " + getFullTableName() + " WHERE session_id = ? AND state_key = ?";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] For list states, save(SessionKey, String, List) writes both state_key = key (one row per item) and an auxiliary state_key = key + ":_hash" row used by needsFullRewrite(...). This new delete(SessionKey, String key) only removes rows whose state_key exactly equals key, leaving the :_hash row orphaned. Correctness on the next save() is preserved (the prefix-hash mismatch triggers a full rewrite), but the orphan row leaks if no subsequent save happens, and getStoredHash will keep returning a stale value. Suggest deleting both in one statement:

String deleteSql =
        "DELETE FROM " + getFullTableName()
            + " WHERE session_id = ? AND state_key IN (?, ?)";
// ...
stmt.setString(1, sessionId);
stmt.setString(2, key);
stmt.setString(3, key + HASH_KEY_SUFFIX);
stmt.executeUpdate();

try (PreparedStatement stmt = conn.prepareStatement(deleteSql)) {
stmt.setString(1, sessionId);
stmt.setString(2, key);
stmt.executeUpdate();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] stmt.executeUpdate() returns the affected row count, which is currently discarded. Consider capturing it and logging at debug level when zero rows are deleted — it can be useful when debugging "flag clear had no visible effect" reports of the kind this PR was created to fix.

verify(mockStatement).executeUpdate();
}

@Test
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] testDeleteStateKeyThrowsOnFailure exercises the executeUpdate failure path but does not assert that the transaction was rolled back. Adding verify(mockConnection).rollback(); would lock in the rollback guarantee provided by executeInWriteTransaction(...) and protect against future regressions in that helper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core/memory Memory, session, state area/ext/memory Memory/session extension implementations bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants