diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 6e72b5c7..de300d00 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1530,10 +1530,11 @@ public class AgentGraphBuilder { adopting a KB article as the user's project. ## Session Search - - `session_search(agentId, currentConversationId, mode, query, limit)` — search conversation history + - `session_search(agentId, mode, query, limit)` — search conversation history - mode="recent": list recent conversations (titles, times, message counts) - mode="search": keyword full-text search across past messages - Use this to recall previous discussions, look up past decisions, or find context from earlier conversations + - Only completed sessions (not currently running) are included in results ## Tool Usage Guidelines When you have available tools, use them to access local system information, files, or execute commands. diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java index 7710241d..e134adbe 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java @@ -97,8 +97,8 @@ public class StateGraphPlanExecuteAgent extends BaseAgent implements StructuredS log.info("[{}] Plan-Execute replay stream: conversationId={}", agentName, conversationId); Map inputs = buildInitialState(userMessage, conversationId); - // 从 DB 恢复 awaiting_approval 状态的计划上下文 - PlanningService.PlanResumeContext ctx = planningService.findAwaitingApprovalContext(); + // 从 DB 恢复 awaiting_approval 状态的计划上下文(按 conversationId 过滤,避免并发会话误取) + PlanningService.PlanResumeContext ctx = planningService.findAwaitingApprovalContext(conversationId); if (ctx != null) { inputs.put(PlanStateKeys.PLAN_ID, ctx.planId()); inputs.put(PlanStateKeys.PLAN_STEPS, ctx.steps()); diff --git a/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchService.java b/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchService.java index da03c709..2523a32a 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchService.java @@ -58,13 +58,17 @@ public class SessionSearchService { /** * List recent conversations for the given agent. + * Excludes the current conversation and any still-running sessions to prevent + * cross-conversation context leakage in concurrent multi-conversation scenarios. */ - public List> listRecent(Long agentId, int limit) { + public List> listRecent(Long agentId, String currentConversationId, int limit) { int effectiveLimit = Math.min(Math.max(limit, 1), 50); String sql = """ SELECT conversation_id, title, message_count, last_active_time, create_time FROM mate_conversation WHERE agent_id = ? AND deleted = 0 + AND conversation_id != ? + AND (stream_status IS NULL OR stream_status != 'running') ORDER BY last_active_time DESC LIMIT ? """; @@ -77,7 +81,7 @@ public class SessionSearchService { row.put("lastActiveTime", toLocalDateTime(rs.getTimestamp("last_active_time"))); row.put("createTime", toLocalDateTime(rs.getTimestamp("create_time"))); return row; - }, agentId, effectiveLimit); + }, agentId, currentConversationId != null ? currentConversationId : "", effectiveLimit); } // ==================== MySQL FULLTEXT ==================== @@ -93,6 +97,7 @@ public class SessionSearchService { WHERE c.agent_id = ? AND m.conversation_id != ? AND m.role IN ('user', 'assistant') AND m.deleted = 0 AND c.deleted = 0 + AND (c.stream_status IS NULL OR c.stream_status != 'running') AND MATCH(m.content) AGAINST(? IN NATURAL LANGUAGE MODE) ORDER BY relevance DESC LIMIT ? @@ -116,6 +121,7 @@ public class SessionSearchService { WHERE c.agent_id = ? AND m.conversation_id != ? AND m.role IN ('user', 'assistant') AND m.deleted = 0 AND c.deleted = 0 + AND (c.stream_status IS NULL OR c.stream_status != 'running') AND LOWER(m.content) LIKE LOWER(CONCAT('%', ?, '%')) ORDER BY m.create_time DESC LIMIT ? diff --git a/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchTool.java b/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchTool.java index 792b1b8b..12bc7c7d 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchTool.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/search/SessionSearchTool.java @@ -4,9 +4,11 @@ import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; +import vip.mate.agent.context.ChatOrigin; import java.util.List; import java.util.Map; @@ -33,13 +35,14 @@ public class SessionSearchTool { - "recent":列出最近的会话(标题、时间、消息数),不需要 query 参数 - "search":按关键词全文搜索消息内容,返回匹配的消息片段 适用于回忆之前讨论过的话题、查找历史决策、检索之前的上下文。 + 注意:只会搜索已完成的会话,不会返回当前正在运行中的其他会话内容。 """) public String session_search( @ToolParam(description = "当前 Agent 的 ID") Long agentId, - @ToolParam(description = "当前会话 ID(用于排除当前会话)") String currentConversationId, @ToolParam(description = "搜索模式:recent 或 search") String mode, @ToolParam(description = "搜索关键词(mode=search 时必填)", required = false) String query, - @ToolParam(description = "返回结果数量上限,默认 10", required = false) Integer limit) { + @ToolParam(description = "返回结果数量上限,默认 10", required = false) Integer limit, + ToolContext toolContext) { if (agentId == null) { return error("agentId 不能为空"); @@ -48,11 +51,20 @@ public class SessionSearchTool { mode = "recent"; } + // 从 ToolContext 强制读取当前会话 ID(替代 LLM 自传,防止并发会话记忆混乱) + String currentConversationId = ""; + if (toolContext != null) { + String fromOrigin = ChatOrigin.from(toolContext).conversationId(); + if (fromOrigin != null && !fromOrigin.isEmpty()) { + currentConversationId = fromOrigin; + } + } + int effectiveLimit = limit != null && limit > 0 ? limit : 10; try { if ("recent".equalsIgnoreCase(mode.trim())) { - return handleRecent(agentId, effectiveLimit); + return handleRecent(agentId, currentConversationId, effectiveLimit); } else if ("search".equalsIgnoreCase(mode.trim())) { if (query == null || query.isBlank()) { return error("mode=search 时 query 不能为空"); @@ -67,8 +79,8 @@ public class SessionSearchTool { } } - private String handleRecent(Long agentId, int limit) { - List> sessions = sessionSearchService.listRecent(agentId, limit); + private String handleRecent(Long agentId, String currentConversationId, int limit) { + List> sessions = sessionSearchService.listRecent(agentId, currentConversationId, limit); JSONObject result = new JSONObject(); result.set("mode", "recent"); diff --git a/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java b/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java index 9ef9ed6f..fac3ac82 100644 --- a/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java +++ b/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java @@ -224,12 +224,22 @@ public class PlanningService { /** * 审批 replay 上下文:找到最近一条 running 且含 awaiting_approval 步骤的计划, * 返回恢复图执行所需的全部状态。 + *

+ * 按 conversationId 过滤,防止并发会话误取兄弟会话的计划。 */ - public PlanResumeContext findAwaitingApprovalContext() { - PlanEntity plan = planMapper.selectOne(new LambdaQueryWrapper() + public PlanResumeContext findAwaitingApprovalContext(String conversationId) { + if (conversationId == null || conversationId.isBlank()) { + log.warn("[PlanningService] findAwaitingApprovalContext called without conversationId, " + + "risk of cross-conversation plan pickup in concurrent scenarios"); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(PlanEntity::getStatus, "running") .orderByDesc(PlanEntity::getCreateTime) - .last("LIMIT 1")); + .last("LIMIT 1"); + if (conversationId != null && !conversationId.isBlank()) { + wrapper.eq(PlanEntity::getConversationId, conversationId); + } + PlanEntity plan = planMapper.selectOne(wrapper); if (plan == null) return null; List subPlans = subPlanMapper.selectList(